Skip to content

Commit 1b0851c

Browse files
authored
feat: add lib and examples (#36)
1 parent f9e2a50 commit 1b0851c

12 files changed

+1303
-4
lines changed

.devcontainer/devcontainer.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@
66
"features": {
77
"ghcr.io/devcontainers/features/node:1": {}
88
},
9-
"postCreateCommand": "yarn install"
9+
"postCreateCommand": "yarn install",
10+
"customizations": {
11+
"vscode": {
12+
"extensions": [
13+
"esbenp.prettier-vscode"
14+
]
15+
}
16+
}
1017
}

examples/cleanup.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Disposables } from '../src/lib/disposables';
2+
3+
/**
4+
* A utility function that wraps the main logic with proper signal handling and cleanup.
5+
* It ensures that disposables are cleaned up properly when the process is interrupted.
6+
*
7+
* @param fn The main function that receives disposables and returns a promise
8+
* @returns A promise that resolves to the result of the function
9+
*/
10+
export async function withCleanup<T>(fn: (disposables: Disposables) => Promise<T>): Promise<T> {
11+
let disposablesCleanup: (() => Promise<void>) | undefined;
12+
13+
// Setup signal handlers for cleanup
14+
const signalHandler = async (signal: NodeJS.Signals) => {
15+
console.log(`\nReceived ${signal}. Cleaning up...`);
16+
if (disposablesCleanup) {
17+
await disposablesCleanup();
18+
}
19+
process.exit(0);
20+
};
21+
22+
process.on('SIGINT', signalHandler);
23+
process.on('SIGTERM', signalHandler);
24+
process.on('SIGQUIT', signalHandler);
25+
26+
try {
27+
return await Disposables.with(async (disposables) => {
28+
// Store cleanup function for signal handlers
29+
disposablesCleanup = () => disposables.cleanup();
30+
return await fn(disposables);
31+
});
32+
} catch (error) {
33+
console.error('Error:', error);
34+
process.exit(1);
35+
}
36+
}

examples/fs-access.ts

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Gitpod } from '../src/client';
2+
import { findMostUsedEnvironmentClass, EnvironmentState } from '../src/lib/environment';
3+
import { EnvironmentSpec } from '../src/resources/environments/environments';
4+
import { verifyContextUrl } from './scm-auth';
5+
import { generateKeyPairSync } from 'crypto';
6+
import { Client, SFTPWrapper } from 'ssh2';
7+
import { withCleanup } from './cleanup';
8+
import * as sshpk from 'sshpk';
9+
10+
/**
11+
* Examples:
12+
* - yarn ts-node examples/fs-access.ts
13+
* - yarn ts-node examples/fs-access.ts https://github.com/gitpod-io/empty
14+
*/
15+
async function main() {
16+
const contextUrl = process.argv[2];
17+
18+
await withCleanup(async (disposables) => {
19+
const client = new Gitpod({
20+
logLevel: 'info',
21+
});
22+
23+
const envClass = await findMostUsedEnvironmentClass(client);
24+
if (!envClass) {
25+
console.error('Error: No environment class found. Please create one first.');
26+
process.exit(1);
27+
}
28+
console.log(`Found environment class: ${envClass.displayName} (${envClass.description})`);
29+
30+
console.log('Generating SSH key pair');
31+
const { publicKey: pemPublicKey, privateKey: pemPrivateKey } = generateKeyPairSync('rsa', {
32+
modulusLength: 2048,
33+
publicKeyEncoding: {
34+
type: 'spki',
35+
format: 'pem',
36+
},
37+
privateKeyEncoding: {
38+
type: 'pkcs8',
39+
format: 'pem',
40+
},
41+
});
42+
43+
// Convert PEM keys to OpenSSH format
44+
const keyObject = sshpk.parseKey(pemPublicKey, 'pem');
45+
const publicKey = keyObject.toString('ssh');
46+
47+
const privateKeyObject = sshpk.parsePrivateKey(pemPrivateKey, 'pem');
48+
const privateKey = privateKeyObject.toString('ssh');
49+
50+
console.log('Creating environment with SSH access');
51+
const keyId = 'fs-access-example';
52+
const spec: EnvironmentSpec = {
53+
desiredPhase: 'ENVIRONMENT_PHASE_RUNNING',
54+
machine: { class: envClass.id },
55+
sshPublicKeys: [
56+
{
57+
id: keyId,
58+
value: publicKey,
59+
},
60+
],
61+
};
62+
63+
if (contextUrl) {
64+
await verifyContextUrl(client, contextUrl, envClass.runnerId);
65+
spec.content = {
66+
initializer: {
67+
specs: [
68+
{
69+
contextUrl: {
70+
url: contextUrl,
71+
},
72+
},
73+
],
74+
},
75+
};
76+
}
77+
78+
console.log('Creating environment');
79+
const { environment } = await client.environments.create({ spec });
80+
disposables.add(() => client.environments.delete({ environmentId: environment.id }));
81+
82+
const env = new EnvironmentState(client, environment.id);
83+
disposables.add(() => env.close());
84+
85+
console.log('Waiting for environment to be running');
86+
await env.waitUntilRunning();
87+
88+
console.log('Waiting for SSH key to be applied');
89+
await env.waitForSshKeyApplied(keyId, publicKey);
90+
91+
console.log('Waiting for SSH URL');
92+
const sshUrl = await env.waitForSshUrl();
93+
94+
console.log(`Setting up SSH connection to ${sshUrl}`);
95+
// Parse ssh://username@host:port format
96+
const urlParts = sshUrl.split('://')[1];
97+
if (!urlParts) {
98+
throw new Error('Invalid SSH URL format');
99+
}
100+
101+
const [username, rest] = urlParts.split('@');
102+
if (!username || !rest) {
103+
throw new Error('Invalid SSH URL format: missing username or host');
104+
}
105+
106+
const [host, portStr] = rest.split(':');
107+
if (!host || !portStr) {
108+
throw new Error('Invalid SSH URL format: missing host or port');
109+
}
110+
111+
const port = parseInt(portStr, 10);
112+
if (isNaN(port)) {
113+
throw new Error('Invalid SSH URL format: invalid port number');
114+
}
115+
116+
const ssh = new Client();
117+
disposables.add(() => ssh.end());
118+
119+
await new Promise<void>((resolve, reject) => {
120+
ssh.on('ready', resolve);
121+
ssh.on('error', reject);
122+
123+
ssh.connect({
124+
host,
125+
port,
126+
username,
127+
privateKey,
128+
});
129+
});
130+
131+
console.log('Creating SFTP client');
132+
const sftp = await new Promise<SFTPWrapper>((resolve, reject) => {
133+
ssh.sftp((err, sftp) => {
134+
if (err) reject(err);
135+
else resolve(sftp);
136+
});
137+
});
138+
disposables.add(() => sftp.end());
139+
140+
console.log('Writing test file');
141+
const testContent = 'Hello from Gitpod TypeScript SDK!';
142+
await new Promise<void>((resolve, reject) => {
143+
sftp.writeFile('test.txt', Buffer.from(testContent), (err) => {
144+
if (err) reject(err);
145+
else resolve();
146+
});
147+
});
148+
149+
const content = await new Promise<string>((resolve, reject) => {
150+
sftp.readFile('test.txt', (err, data) => {
151+
if (err) reject(err);
152+
else resolve(data.toString());
153+
});
154+
});
155+
console.log(`File content: ${content}`);
156+
});
157+
}
158+
159+
main().catch((error) => {
160+
console.error('Error:', error);
161+
process.exit(1);
162+
});

examples/run-command.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Gitpod } from '../src/client';
2+
import { findMostUsedEnvironmentClass, waitForEnvironmentRunning } from '../src/lib/environment';
3+
import { runCommand } from '../src/lib/automation';
4+
import { EnvironmentSpec } from '../src/resources/environments/environments';
5+
import { verifyContextUrl } from './scm-auth';
6+
import { withCleanup } from './cleanup';
7+
8+
/**
9+
* Examples:
10+
* - yarn ts-node examples/run-command.ts 'echo "Hello World!"'
11+
* - yarn ts-node examples/run-command.ts 'echo "Hello World!"' https://github.com/gitpod-io/empty
12+
*/
13+
async function main() {
14+
const args = process.argv.slice(2);
15+
if (args.length < 1) {
16+
console.log('Usage: yarn ts-node examples/run-command.ts "<COMMAND>" [CONTEXT_URL]');
17+
process.exit(1);
18+
}
19+
20+
const command = args[0];
21+
const contextUrl = args[1];
22+
23+
await withCleanup(async (disposables) => {
24+
const client = new Gitpod({
25+
logLevel: 'info',
26+
});
27+
28+
const envClass = await findMostUsedEnvironmentClass(client);
29+
if (!envClass) {
30+
console.error('Error: No environment class found. Please create one first.');
31+
process.exit(1);
32+
}
33+
console.log(`Found environment class: ${envClass.displayName} (${envClass.description})`);
34+
35+
const spec: EnvironmentSpec = {
36+
desiredPhase: 'ENVIRONMENT_PHASE_RUNNING',
37+
machine: { class: envClass.id },
38+
};
39+
40+
if (contextUrl) {
41+
await verifyContextUrl(client, contextUrl, envClass.runnerId);
42+
spec.content = {
43+
initializer: {
44+
specs: [
45+
{
46+
contextUrl: {
47+
url: contextUrl,
48+
},
49+
},
50+
],
51+
},
52+
};
53+
}
54+
55+
console.log('Creating environment');
56+
const { environment } = await client.environments.create({ spec });
57+
disposables.add(() => client.environments.delete({ environmentId: environment.id }));
58+
59+
console.log('Waiting for environment to be ready');
60+
await waitForEnvironmentRunning(client, environment.id);
61+
62+
console.log('Running command');
63+
const lines = await runCommand(client, environment.id, command!);
64+
for await (const line of lines) {
65+
console.log(line);
66+
}
67+
});
68+
}
69+
70+
main().catch((error) => {
71+
console.error('Error:', error);
72+
process.exit(1);
73+
});

examples/run-service.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Gitpod } from '../src/client';
2+
import { findMostUsedEnvironmentClass, EnvironmentState } from '../src/lib/environment';
3+
import { runService } from '../src/lib/automation';
4+
import { EnvironmentSpec } from '../src/resources/environments/environments';
5+
import { verifyContextUrl } from './scm-auth';
6+
import { withCleanup } from './cleanup';
7+
8+
/**
9+
* Examples:
10+
* - yarn ts-node examples/run-service.ts
11+
* - yarn ts-node examples/run-service.ts https://github.com/gitpod-io/empty
12+
*/
13+
async function main() {
14+
const contextUrl = process.argv[2];
15+
16+
await withCleanup(async (disposables) => {
17+
const client = new Gitpod({
18+
logLevel: 'info',
19+
});
20+
21+
const envClass = await findMostUsedEnvironmentClass(client);
22+
if (!envClass) {
23+
console.error('Error: No environment class found. Please create one first.');
24+
process.exit(1);
25+
}
26+
console.log(`Found environment class: ${envClass.displayName} (${envClass.description})`);
27+
28+
const port = 8888;
29+
const spec: EnvironmentSpec = {
30+
desiredPhase: 'ENVIRONMENT_PHASE_RUNNING',
31+
machine: { class: envClass.id },
32+
ports: [
33+
{
34+
name: 'Lama Service',
35+
port,
36+
admission: 'ADMISSION_LEVEL_EVERYONE',
37+
},
38+
],
39+
};
40+
41+
if (contextUrl) {
42+
await verifyContextUrl(client, contextUrl, envClass.runnerId);
43+
spec.content = {
44+
initializer: {
45+
specs: [
46+
{
47+
contextUrl: {
48+
url: contextUrl,
49+
},
50+
},
51+
],
52+
},
53+
};
54+
}
55+
56+
console.log('Creating environment');
57+
const { environment } = await client.environments.create({ spec });
58+
disposables.add(() => client.environments.delete({ environmentId: environment.id }));
59+
60+
console.log('Waiting for environment to be ready');
61+
const env = new EnvironmentState(client, environment.id);
62+
disposables.add(() => env.close());
63+
await env.waitUntilRunning();
64+
65+
console.log('Starting Lama Service');
66+
const lines = await runService(
67+
client,
68+
environment.id,
69+
{
70+
name: 'Lama Service',
71+
description: 'Lama Service',
72+
reference: 'lama-service',
73+
},
74+
{
75+
commands: {
76+
start: `curl lama.sh | LAMA_PORT=${port} sh`,
77+
ready: `curl -s http://localhost:${port}`,
78+
},
79+
},
80+
);
81+
82+
const portUrl = await env.waitForPortUrl(port);
83+
console.log(`Lama Service is running at ${portUrl}`);
84+
85+
for await (const line of lines) {
86+
console.log(line);
87+
}
88+
});
89+
}
90+
91+
main().catch((error) => {
92+
console.error('Error:', error);
93+
process.exit(1);
94+
});

0 commit comments

Comments
 (0)