Skip to content

Commit

Permalink
feat: add lib and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov committed Feb 21, 2025
1 parent f9e2a50 commit 15fb112
Show file tree
Hide file tree
Showing 12 changed files with 1,303 additions and 4 deletions.
9 changes: 8 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@
"features": {
"ghcr.io/devcontainers/features/node:1": {}
},
"postCreateCommand": "yarn install"
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode"
]
}
}
}
36 changes: 36 additions & 0 deletions examples/cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Disposables } from '../src/lib/disposables';

/**
* A utility function that wraps the main logic with proper signal handling and cleanup.
* It ensures that disposables are cleaned up properly when the process is interrupted.
*
* @param fn The main function that receives disposables and returns a promise
* @returns A promise that resolves to the result of the function
*/
export async function withCleanup<T>(fn: (disposables: Disposables) => Promise<T>): Promise<T> {
let disposablesCleanup: (() => Promise<void>) | undefined;

// Setup signal handlers for cleanup
const signalHandler = async (signal: NodeJS.Signals) => {
console.log(`\nReceived ${signal}. Cleaning up...`);
if (disposablesCleanup) {
await disposablesCleanup();
}
process.exit(0);
};

process.on('SIGINT', signalHandler);
process.on('SIGTERM', signalHandler);
process.on('SIGQUIT', signalHandler);

try {
return await Disposables.with(async (disposables) => {
// Store cleanup function for signal handlers
disposablesCleanup = () => disposables.cleanup();
return await fn(disposables);
});
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
162 changes: 162 additions & 0 deletions examples/fs-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Gitpod } from '../src/client';
import { findMostUsedEnvironmentClass, EnvironmentState } from '../src/lib/environment';
import { EnvironmentSpec } from '../src/resources/environments/environments';
import { verifyContextUrl } from './scm-auth';
import { generateKeyPairSync } from 'crypto';
import { Client, SFTPWrapper } from 'ssh2';
import { withCleanup } from './cleanup';
import * as sshpk from 'sshpk';

/**
* Examples:
* - yarn ts-node examples/fs-access.ts
* - yarn ts-node examples/fs-access.ts https://github.com/gitpod-io/empty
*/
async function main() {
const contextUrl = process.argv[2];

await withCleanup(async (disposables) => {
const client = new Gitpod({
logLevel: 'info',
});

const envClass = await findMostUsedEnvironmentClass(client);
if (!envClass) {
console.error('Error: No environment class found. Please create one first.');
process.exit(1);
}
console.log(`Found environment class: ${envClass.displayName} (${envClass.description})`);

console.log('Generating SSH key pair');
const { publicKey: pemPublicKey, privateKey: pemPrivateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});

// Convert PEM keys to OpenSSH format
const keyObject = sshpk.parseKey(pemPublicKey, 'pem');
const publicKey = keyObject.toString('ssh');

const privateKeyObject = sshpk.parsePrivateKey(pemPrivateKey, 'pem');
const privateKey = privateKeyObject.toString('ssh');

console.log('Creating environment with SSH access');
const keyId = 'fs-access-example';
const spec: EnvironmentSpec = {
desiredPhase: 'ENVIRONMENT_PHASE_RUNNING',
machine: { class: envClass.id },
sshPublicKeys: [
{
id: keyId,
value: publicKey,
},
],
};

if (contextUrl) {
await verifyContextUrl(client, contextUrl, envClass.runnerId);
spec.content = {
initializer: {
specs: [
{
contextUrl: {
url: contextUrl,
},
},
],
},
};
}

console.log('Creating environment');
const { environment } = await client.environments.create({ spec });
disposables.add(() => client.environments.delete({ environmentId: environment.id }));

const env = new EnvironmentState(client, environment.id);
disposables.add(() => env.close());

console.log('Waiting for environment to be running');
await env.waitUntilRunning();

console.log('Waiting for SSH key to be applied');
await env.waitForSshKeyApplied(keyId, publicKey);

console.log('Waiting for SSH URL');
const sshUrl = await env.waitForSshUrl();

console.log(`Setting up SSH connection to ${sshUrl}`);
// Parse ssh://username@host:port format
const urlParts = sshUrl.split('://')[1];
if (!urlParts) {
throw new Error('Invalid SSH URL format');
}

const [username, rest] = urlParts.split('@');
if (!username || !rest) {
throw new Error('Invalid SSH URL format: missing username or host');
}

const [host, portStr] = rest.split(':');
if (!host || !portStr) {
throw new Error('Invalid SSH URL format: missing host or port');
}

const port = parseInt(portStr, 10);
if (isNaN(port)) {
throw new Error('Invalid SSH URL format: invalid port number');
}

const ssh = new Client();
disposables.add(() => ssh.end());

await new Promise<void>((resolve, reject) => {
ssh.on('ready', resolve);
ssh.on('error', reject);

ssh.connect({
host,
port,
username,
privateKey,
});
});

console.log('Creating SFTP client');
const sftp = await new Promise<SFTPWrapper>((resolve, reject) => {
ssh.sftp((err, sftp) => {
if (err) reject(err);
else resolve(sftp);
});
});
disposables.add(() => sftp.end());

console.log('Writing test file');
const testContent = 'Hello from Gitpod TypeScript SDK!';
await new Promise<void>((resolve, reject) => {
sftp.writeFile('test.txt', Buffer.from(testContent), (err) => {
if (err) reject(err);
else resolve();
});
});

const content = await new Promise<string>((resolve, reject) => {
sftp.readFile('test.txt', (err, data) => {
if (err) reject(err);
else resolve(data.toString());
});
});
console.log(`File content: ${content}`);
});
}

main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
73 changes: 73 additions & 0 deletions examples/run-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Gitpod } from '../src/client';
import { findMostUsedEnvironmentClass, waitForEnvironmentRunning } from '../src/lib/environment';
import { runCommand } from '../src/lib/automation';
import { EnvironmentSpec } from '../src/resources/environments/environments';
import { verifyContextUrl } from './scm-auth';
import { withCleanup } from './cleanup';

/**
* Examples:
* - yarn ts-node examples/run-command.ts 'echo "Hello World!"'
* - yarn ts-node examples/run-command.ts 'echo "Hello World!"' https://github.com/gitpod-io/empty
*/
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('Usage: yarn ts-node examples/run-command.ts "<COMMAND>" [CONTEXT_URL]');
process.exit(1);
}

const command = args[0];
const contextUrl = args[1];

await withCleanup(async (disposables) => {
const client = new Gitpod({
logLevel: 'info',
});

const envClass = await findMostUsedEnvironmentClass(client);
if (!envClass) {
console.error('Error: No environment class found. Please create one first.');
process.exit(1);
}
console.log(`Found environment class: ${envClass.displayName} (${envClass.description})`);

const spec: EnvironmentSpec = {
desiredPhase: 'ENVIRONMENT_PHASE_RUNNING',
machine: { class: envClass.id },
};

if (contextUrl) {
await verifyContextUrl(client, contextUrl, envClass.runnerId);
spec.content = {
initializer: {
specs: [
{
contextUrl: {
url: contextUrl,
},
},
],
},
};
}

console.log('Creating environment');
const { environment } = await client.environments.create({ spec });
disposables.add(() => client.environments.delete({ environmentId: environment.id }));

console.log('Waiting for environment to be ready');
await waitForEnvironmentRunning(client, environment.id);

console.log('Running command');
const lines = await runCommand(client, environment.id, command!);
for await (const line of lines) {
console.log(line);
}
});
}

main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
94 changes: 94 additions & 0 deletions examples/run-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Gitpod } from '../src/client';
import { findMostUsedEnvironmentClass, EnvironmentState } from '../src/lib/environment';
import { runService } from '../src/lib/automation';
import { EnvironmentSpec } from '../src/resources/environments/environments';
import { verifyContextUrl } from './scm-auth';
import { withCleanup } from './cleanup';

/**
* Examples:
* - yarn ts-node examples/run-service.ts
* - yarn ts-node examples/run-service.ts https://github.com/gitpod-io/empty
*/
async function main() {
const contextUrl = process.argv[2];

await withCleanup(async (disposables) => {
const client = new Gitpod({
logLevel: 'info',
});

const envClass = await findMostUsedEnvironmentClass(client);
if (!envClass) {
console.error('Error: No environment class found. Please create one first.');
process.exit(1);
}
console.log(`Found environment class: ${envClass.displayName} (${envClass.description})`);

const port = 8888;
const spec: EnvironmentSpec = {
desiredPhase: 'ENVIRONMENT_PHASE_RUNNING',
machine: { class: envClass.id },
ports: [
{
name: 'Lama Service',
port,
admission: 'ADMISSION_LEVEL_EVERYONE',
},
],
};

if (contextUrl) {
await verifyContextUrl(client, contextUrl, envClass.runnerId);
spec.content = {
initializer: {
specs: [
{
contextUrl: {
url: contextUrl,
},
},
],
},
};
}

console.log('Creating environment');
const { environment } = await client.environments.create({ spec });
disposables.add(() => client.environments.delete({ environmentId: environment.id }));

console.log('Waiting for environment to be ready');
const env = new EnvironmentState(client, environment.id);
disposables.add(() => env.close());
await env.waitUntilRunning();

console.log('Starting Lama Service');
const lines = await runService(
client,
environment.id,
{
name: 'Lama Service',
description: 'Lama Service',
reference: 'lama-service',
},
{
commands: {
start: `curl lama.sh | LAMA_PORT=${port} sh`,
ready: `curl -s http://localhost:${port}`,
},
},
);

const portUrl = await env.waitForPortUrl(port);
console.log(`Lama Service is running at ${portUrl}`);

for await (const line of lines) {
console.log(line);
}
});
}

main().catch((error) => {
console.error('Error:', error);
process.exit(1);
});
Loading

0 comments on commit 15fb112

Please # to comment.