Skip to content

Commit

Permalink
API: add post/delete commands for port forwarding
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Seese <[email protected]>
  • Loading branch information
mikeseese committed Apr 5, 2024
1 parent 17aff4e commit 6ea9ff5
Show file tree
Hide file tree
Showing 4 changed files with 173 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
18 changes: 18 additions & 0 deletions bats/tests/k8s/wasm.bats
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,21 @@ EOF
assert_success
assert_output "Hello world from Spin!"
}

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

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

@test 'fail to connect to the service on localhost after removing port forwarding' {
rdctl api -X DELETE "port_forwarding?namespace=default&service=hello-spin&k8sPort=80"
run try curl --silent --fail "http://localhost:8080/hello"
assert_failure
}
41 changes: 41 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,47 @@ 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
hostPort:
type: integer
required: true
responses:
'200':
description: The port forwarding was created.
'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.
'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) {
console.error(`createPortForwarding: error forwarding port:`, err);
response.status(400).type('txt').send('Could not forward port');
}
} 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 6ea9ff5

Please sign in to comment.