Skip to content

feat: add lib and examples #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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