8000 Add deploy cli [WIP] (#59) · scripthon/deploy-code-server@0cedc14 · GitHub
  • [go: up one dir, main page]

    Skip to content

    Commit 0cedc14

    Browse files
    BrunoQuaresmabpmct
    andauthored
    Add deploy cli [WIP] (coder#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)
    0