Skip to content

Commit

Permalink
Merge pull request #6687 from Incanta/feat/port-forwarding-api
Browse files Browse the repository at this point in the history
API: add post/delete commands for port forwarding
  • Loading branch information
mook-as authored Apr 10, 2024
2 parents 312aeaa + 63577d9 commit d15e78e
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 2 deletions.
20 changes: 18 additions & 2 deletions background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,12 +842,20 @@ ipcMainProxy.handle('service-forward', async(_, service, state) => {
if (state) {
const hostPort = service.listenPort ?? 0;

await k8smanager.kubeBackend.forwardPort(namespace, service.name, service.port, hostPort);
await doForwardPort(namespace, service.name, service.port, hostPort);
} else {
await k8smanager.kubeBackend.cancelForward(namespace, service.name, service.port);
await doCancelForward(namespace, service.name, service.port);
}
});

async function doForwardPort(namespace: string, service: string, k8sPort: string | number, hostPort: number) {
return await k8smanager.kubeBackend.forwardPort(namespace, service, k8sPort, hostPort);
}

async function doCancelForward(namespace: string, service: string, k8sPort: string | number) {
return await k8smanager.kubeBackend.cancelForward(namespace, service, k8sPort);
}

ipcMainProxy.on('k8s-integrations', async() => {
mainEvents.emit('integration-update', await integrationManager.listIntegrations() ?? {});
});
Expand Down Expand Up @@ -1354,6 +1362,14 @@ class BackgroundCommandWorker implements CommandWorkerInterface {
doFactoryReset(keepSystemImages);
}

async forwardPort(namespace: string, service: string, k8sPort: string | number, hostPort: number) {
return await doForwardPort(namespace, service, k8sPort, hostPort);
}

async cancelForward(namespace: string, service: string, k8sPort: string | number) {
return await doCancelForward(namespace, service, k8sPort);
}

/**
* Execute the preference update for services that don't require a backend restart.
*/
Expand Down
98 changes: 98 additions & 0 deletions bats/tests/k8s/port-forwarding.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
load '../helpers/load'

@test 'start k8s' {
factory_reset
start_kubernetes
wait_for_kubelet
}

@test 'deploy sample app' {
kubectl apply --filename - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-configmap
data:
index: "Hello World!"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
volumes:
- name: webapp-config-volume
configMap:
name: webapp-configmap
items:
- key: index
path: index.html
containers:
- name: webapp
image: nginx
volumeMounts:
- name: webapp-config-volume
mountPath: /usr/share/nginx/html
EOF
}

@test 'deploy ingress' {
kubectl apply --filename - <<EOF
apiVersion: v1
kind: Service
metadata:
name: webapp
spec:
type: ClusterIP
selector:
app: webapp
ports:
- port: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webapp
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: webapp
port:
number: 80
EOF
}

@test 'fail to connect to the service on localhost without port forwarding' {
run try --max 5 curl --silent --fail "http://localhost:8080"
assert_failure
}

@test 'connect to the service on localhost with port forwarding' {
rdctl api -X POST -b '{ "namespace": "default", "service": "webapp", "k8sPort": 80, "hostPort": 8080 }' port_forwarding
run try curl --silent --fail "http://localhost:8080"
assert_success
assert_output "Hello World!"
}

@test 'fail to connect to the service on localhost after removing port forwarding' {
rdctl api -X DELETE "port_forwarding?namespace=default&service=webapp&k8sPort=80"
run try --max 5 curl --silent --fail "http://localhost:8080"
assert_failure
}
47 changes: 47 additions & 0 deletions pkg/rancher-desktop/assets/specs/command-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,53 @@ paths:
'400':
description: An error occurred

/v1/port_forwarding:
post:
operationId: createPortForward
summary: Create a new port forwarding
requestBody:
description: JSON block consisting of the port forwarding details
content:
application/json:
schema:
type: object
properties:
namespace:
type: string
service:
type: string
k8sPort:
type:
- string
- integer
hostPort:
type: integer
required: true
responses:
'200':
description: The port forwarding was created or already exists; the response contains the listening host port.
content:
text/plain:
schema:
type: integer
'400':
description: The port forwarding could not be created.
delete:
operationId: deletePortForward
summary: Delete a port forwarding
parameters:
- in: query
name: namespace
- in: query
name: service
- in: query
name: k8sPort
responses:
'200':
description: The port forwarding was deleted or doesn't exist.
'400':
description: The port forwarding could not be deleted.

/v1/propose_settings:
put:
operationId: proposeSettings
Expand Down
96 changes: 96 additions & 0 deletions pkg/rancher-desktop/main/commandServer/httpCommandServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export class HttpCommandServer {
},
delete: { '/v1/snapshots': [0, this.deleteSnapshot] },
} as const,
{
post: { '/v1/port_forwarding': [1, this.createPortForwarding] },
delete: { '/v1/port_forwarding': [1, this.deletePortForwarding] },
} as const,
);

constructor(commandWorker: CommandWorkerInterface) {
Expand Down Expand Up @@ -548,6 +552,95 @@ export class HttpCommandServer {
}
}

protected async createPortForwarding(request: express.Request, response: express.Response, _: commandContext): Promise<void> {
let values: Record<string, any> = {};
const [data, payloadError] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);
let error = '';
let namespace = '';
let service = '';
let k8sPort: string | number = 0;
let hostPort = 0;

if (!payloadError) {
try {
console.debug(`Request data: ${ data }`);
values = JSON.parse(data);
if ('namespace' in values && 'service' in values && 'k8sPort' in values && 'hostPort' in values) {
namespace = values.namespace;

service = values.service;

if (Number.isNaN(values.k8sPort)) {
k8sPort = values.k8sPort;
} else {
k8sPort = parseInt(values.k8sPort, 10);
}

hostPort = values.hostPort;
} else {
error = 'missing required parameters';
}
} catch (err) {
// TODO: Revisit this log stmt if sensitive values (e.g. PII, IPs, creds) can be provided via this command
console.log(`updateSettings: error processing JSON request block\n${ data }\n`, err);
error = 'error processing JSON request block';
}
} else {
error = payloadError;
}
if (!error) {
try {
const result = await this.commandWorker.forwardPort(namespace, service, k8sPort, hostPort);

if (typeof result === 'number') {
console.debug('createPortForwarding: succeeded 200');
response.status(200).type('txt').send(`${ result }`);
} else {
console.debug(`createPortForwarding: write back status 400, error forwarding port`);
response.status(400).type('txt').send('Could not forward port');
}
} catch (err: any) {
console.error(`createPortForwarding: error forwarding port:`, err);
response.status(400).type('txt').send(`Could not forward port; error code: ${ typeof err.code === 'string' ? err.code : 'unknown, check the logs' }`);
}
} else {
console.debug(`createPortForwarding: write back status 400, error: ${ error }`);
response.status(400).type('txt').send(error);
}
}

protected async deletePortForwarding(request: express.Request, response: express.Response, context: commandContext): Promise<void> {
const namespace = request.query.namespace ?? '';
const service = request.query.service ?? '';
const k8sPort = request.query.k8sPort ?? '';

if (!namespace) {
response.status(400).type('txt').send('Port forwarding namespace is required in query parameters');
} else if (!service) {
response.status(400).type('txt').send('Port forwarding service is required in query parameters');
} else if (!k8sPort) {
response.status(400).type('txt').send('Port forwarding k8sPort is required in query parameters');
} else if (typeof namespace !== 'string') {
response.status(400).type('txt').send(`Invalid port forwarding namespace ${ JSON.stringify(namespace) }: not a string.`);
} else if (typeof service !== 'string') {
response.status(400).type('txt').send(`Invalid port forwarding service ${ JSON.stringify(service) }: not a string.`);
} else if (typeof k8sPort !== 'string') {
response.status(400).type('txt').send(`Invalid port forwarding k8sPort ${ JSON.stringify(k8sPort) }: not a string.`);
} else {
const k8sPortResolved = Number.isNaN(k8sPort) ? k8sPort : parseInt(k8sPort, 10);

try {
await this.commandWorker.cancelForward(namespace, service, k8sPortResolved);

console.debug('deletePortForwarding: succeeded 200');
response.status(200).type('txt').send('Port forwarding successfully deleted');
} catch (error: any) {
console.error(`deletePortForwarding: error deleting port forwarding:`, error);
response.status(400).type('txt').send('Could not delete port forwarding');
}
}
}

wrapShutdown(request: express.Request, response: express.Response, context: commandContext): Promise<void> {
console.debug('shutdown: succeeded 202');
response.status(202).type('txt').send('Shutting down.');
Expand Down Expand Up @@ -819,6 +912,9 @@ export interface CommandWorkerInterface {
deleteSnapshot: (context: commandContext, name: string) => Promise<void>;
restoreSnapshot: (context: commandContext, name: string) => Promise<void>;
cancelSnapshot: () => Promise<void>;

forwardPort: (namespace: string, service: string, k8sPort: string | number, hostPort: number) => Promise<number | undefined>;
cancelForward: (namespace: string, service: string, k8sPort: string | number) => Promise<void>;
}

// Extend CommandWorkerInterface to have extra types, as these types are used by
Expand Down

0 comments on commit d15e78e

Please sign in to comment.