Skip to content

Commit 0cedc14

Browse files
BrunoQuaresmabpmct
andauthored
Add deploy cli [WIP] (#59)
* Add deploy cli * add development instructions * Fix async runner and add extra token info * Add prettier * Move prettier config * Add Prettier and pre-commit linting * Remove package.json * Move Prettier config to cli * Pull version from package.json * Get description from package.json * Update package info * Update package name * Add cli to coder org Co-authored-by: Ben Potter <me@bpmct.net>
1 parent 8fcc596 commit 0cedc14

File tree

14 files changed

+1518
-0
lines changed

14 files changed

+1518
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
bin

.husky/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_

.husky/pre-commit

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
4+
npx lint-staged

cli/.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
bin
3+
yarn.lock

cli/.prettierrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"tabWidth": 2,
3+
"useTabs": false
4+
}

cli/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# dcs-cli
2+
3+
Provision a code-server instance from your terminal.
4+
5+
## Development
6+
7+
```console
8+
git clone git@github.com:cdr/deploy-code-server.git
9+
cd deploy-code-server/cli
10+
npm install && npm run build:watch
11+
12+
# in another session:
13+
node bin/index.js
14+
```

cli/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@coder/deploy-code-server",
3+
"version": "0.1.0",
4+
"repository": "cdr/deploy-code-server",
5+
"homepage": "https://github.com/cdr/deploy-code-server",
6+
"description": "CLI to deploy code-server",
7+
"main": "bin/index.js",
8+
"bin": "bin/index.js",
9+
"scripts": {
10+
"build": "tsc",
11+
"build:watch": "tsc -w",
12+
"prepare": "yarn build"
13+
},
14+
"keywords": ["code-server", "coder"],
15+
"author": "coder",
16+
"publishConfig": {
17+
"access": "public"
18+
},
19+
"license": "ISC",
20+
"devDependencies": {
21+
"@types/inquirer": "^7.3.3",
22+
"@types/node": "^14.14.20",
23+
"typescript": "^4.1.3"
24+
},
25+
"dependencies": {
26+
"async-wait-until": "^2.0.7",
27+
"chalk": "^4.1.2",
28+
"commander": "^8.1.0",
29+
"got": "^11.8.2",
30+
"inquirer": "^8.1.2",
31+
"ora": "^5.4.1"
32+
}
33+
}

cli/src/deploys/deployDigitalOcean.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import inquirer from "inquirer";
2+
import got from "got";
3+
import ora from "ora";
4+
import chalk from "chalk";
5+
import {
6+
createDroplet,
7+
Droplet,
8+
DropletV4Network,
9+
getDroplet,
10+
} from "../lib/digitalOcean";
11+
import waitUntil from "async-wait-until";
12+
13+
const getUserDataScript = async () =>
14+
got(
15+
"https://raw.githubusercontent.com/cdr/deploy-code-server/main/deploy-vm/launch-code-server.sh"
16+
).text();
17+
18+
const isPermissionError = (error: unknown) => {
19+
return error instanceof got.HTTPError && error.response.statusCode === 401;
20+
};
21+
22+
const getPublicIp = (droplet: Droplet) => {
23+
const network = droplet.networks.v4.find(
24+
(network) => network.type === "public"
25+
);
26+
return network?.ip_address;
27+
};
28+
29+
const isCodeServerLive = async (droplet: Droplet) => {
30+
try {
31+
const response = await got(`http://${getPublicIp(droplet)}`, { retry: 0 });
32+
return response.statusCode === 200;
33+
} catch {
34+
return false;
35+
}
36+
};
37+
38+
const handleErrorLog = (error: unknown) => {
39+
if (isPermissionError(error)) {
40+
console.log(
41+
chalk.red(
42+
chalk.bold("Invalid token."),
43+
"Please, verify your token and try again."
44+
)
45+
);
46+
} else {
47+
console.log(chalk.red.bold("Something wrong happened"));
48+
console.log(
49+
chalk.red(
50+
"You may have to delete the droplet manually on your Digital Ocean dashboard."
51+
)
52+
);
53+
}
54+
};
55+
56+
const oneMinute = 1000 * 60;
57+
const fiveMinutes = oneMinute * 5;
58+
59+
const waitUntilBeActive = (droplet: Droplet, token: string) => {
60+
return waitUntil(
61+
async () => {
62+
const dropletInfo = await getDroplet({ token, id: droplet.id });
63+
return dropletInfo.status === "active";
64+
},
65+
{ timeout: fiveMinutes, intervalBetweenAttempts: oneMinute / 2 }
66+
);
67+
};
68+
69+
const waitUntilHasPublicIp = (droplet: Droplet, token: string) => {
70+
return waitUntil(
71+
async () => {
72+
const dropletInfo = await getDroplet({ token, id: droplet.id });
73+
const ip = getPublicIp(dropletInfo);
74+
return ip !== undefined;
75+
},
76+
{ timeout: fiveMinutes, intervalBetweenAttempts: oneMinute / 2 }
77+
);
78+
};
79+
80+
const waitUntilCodeServerIsLive = (droplet: Droplet, token: string) => {
81+
return waitUntil(
82+
async () => {
83+
const dropletInfo = await getDroplet({ token, id: droplet.id });
84+
return isCodeServerLive(dropletInfo);
85+
},
86+
{ timeout: fiveMinutes * 2, intervalBetweenAttempts: oneMinute / 2 }
87+
);
88+
};
89+
90+
export const deployDigitalOcean = async () => {
91+
let spinner: ora.Ora;
92+
93+
console.log(
94+
chalk.blue(
95+
"You can create a token on",
96+
chalk.bold("https://cloud.digitalocean.com/account/api/tokens")
97+
)
98+
);
99+
const { token } = await inquirer.prompt([
100+
{ name: "token", message: "Your Digital Ocean token:", type: "password" },
101+
]);
102+
103+
try {
104+
let spinner = ora("Creating droplet and installing code-server").start();
105+
let droplet = await createDroplet({
106+
userData: await getUserDataScript(),
107+
token,
108+
});
109+
spinner.stop();
110+
console.log(chalk.green("✅ Droplet created"));
111+
112+
spinner = ora("Waiting droplet to be active").start();
113+
await waitUntilBeActive(droplet, token);
114+
spinner.stop();
115+
console.log(chalk.green("✅ Droplet active"));
116+
117+
spinner = ora("Waiting droplet to have a public IP").start();
118+
await waitUntilHasPublicIp(droplet, token);
119+
spinner.stop();
120+
console.log(chalk.green("✅ Public IP is available"));
121+
122+
spinner = ora(
123+
"Waiting code-server to be live. It can take up to 5 minutes."
124+
).start();
125+
await waitUntilCodeServerIsLive(droplet, token);
126+
droplet = await getDroplet({ token, id: droplet.id });
127+
spinner.stop();
128+
console.log(
129+
chalk.green(
130+
`🚀 Your code-server is live. You can access it on`,
131+
chalk.bold(`http://${getPublicIp(droplet)}`)
132+
)
133+
);
134+
} catch (error) {
135+
spinner.stop();
136+
handleErrorLog(error);
137+
}
138+
};

cli/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
3+
import { program } from "commander";
4+
import { deployDigitalOcean } from "./deploys/deployDigitalOcean";
5+
import packageJson from "../package.json";
6+
7+
const main = async () => {
8+
program.version(packageJson.version).description(packageJson.description);
9+
program.parse();
10+
await deployDigitalOcean();
11+
process.exit(0);
12+
};
13+
14+
main();

cli/src/lib/digitalOcean.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import got from "got";
2+
3+
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com/v2";
4+
5+
export type DropletV4Network = {
6+
ip_address: string;
7+
type: "private" | "public";
8+
};
9+
export type Droplet = {
10+
id: string;
11+
name: string;
12+
networks: { v4: DropletV4Network[] };
13+
status: "new" | "active";
14+
};
15+
16+
type CreateDropletOptions = {
17+
userData: string;
18+
token: string;
19+
};
20+
21+
export const createDroplet = async ({
22+
token,
23+
userData,
24+
}: CreateDropletOptions) => {
25+
return got
26+
.post(`${DIGITALOCEAN_API_URL}/droplets`, {
27+
json: {
28+
name: "code-server",
29+
region: "nyc3",
30+
size: "s-1vcpu-1gb",
31+
image: "ubuntu-20-10-x64",
32+
user_data: userData,
33+
},
34+
headers: {
35+
Authorization: `Bearer ${token}`,
36+
},
37+
})
38+
.json<{ droplet: Droplet }>()
39+
.then((data) => data.droplet);
40+
};
41+
42+
type GetDropletOptions = {
43+
id: string;
44+
token: string;
45+
};
46+
47+
export const getDroplet = async ({ token, id }: GetDropletOptions) => {
48+
return got(`${DIGITALOCEAN_API_URL}/droplets/${id}`, {
49+
headers: {
50+
Authorization: `Bearer ${token}`,
51+
},
52+
})
53+
.json<{ droplet: Droplet }>()
54+
.then((data) => data.droplet);
55+
};

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy