Skip to content

Commit 3a8560b

Browse files
committed
working cli and subprocesses
1 parent 927aeba commit 3a8560b

File tree

5 files changed

+263
-10
lines changed

5 files changed

+263
-10
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"scripts": {
1414
"test": "echo \"Error: no test specified\" && exit 1",
15-
"dev": "nodemon index.js"
15+
"dev": "nodemon bin/launch-coder"
1616
},
1717
"author": "",
1818
"license": "ISC",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gcloud config list --format 'value(core.project)' 2>/dev/null

shell-helpers/googleCloudPrereqs.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Check prereqs. This can potentially search for specific version #s down the road.
2+
3+
if ! [ -x "$(command -v git)" ]; then
4+
echo 'Error: git is not installed.' >&2
5+
exit 1
6+
fi
7+
# if ! [ -x "$(command -v terraform)" ]; then
8+
# echo 'Error: terraform is not installed.' >&2
9+
# exit 1
10+
# fi
11+
if ! [ -x "$(command -v gcloud)" ]; then
12+
echo 'Error: gcloud is not installed.' >&2
13+
exit 1
14+
fi
15+
if ! [ -x "$(command -v kubectl)" ]; then
16+
echo 'Error: kubectl is not installed.' >&2
17+
exit 1
18+
fi
19+
if ! [ -x "$(command -v helm)" ]; then
20+
echo 'Error: helm is not installed.' >&2
21+
exit 1
22+
fi

shell-helpers/googleCloudProjects.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gcloud projects list --format json

src/cli.js

Lines changed: 238 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
11
const yargs = require("yargs/yargs");
22
import inquirer from "inquirer";
3+
import { exit } from "yargs";
34
const { hideBin } = require("yargs/helpers");
45

56
const execa = require("execa");
67

78
require("dotenv").config();
89

10+
const runHelperScript = async (filename, params) => {
11+
try {
12+
let run = await execa("/bin/sh", [
13+
__dirname + `/../shell-helpers/${filename}.sh`,
14+
]);
15+
16+
if (run && run.stdout) {
17+
return run.stdout;
18+
}
19+
} catch (err) {
20+
throw err;
21+
return;
22+
}
23+
};
24+
25+
const generateGoogleClusterCommand = (argv) => {
26+
return `$ gcloud beta container --project "${argv.gcloudProjectId}" \\
27+
clusters create "${argv.gcloudClusterName}" \\
28+
--zone "${argv.gcloudClusterZone}" \\
29+
--no-enable-basic-auth \\
30+
--node-version "latest" \\
31+
--cluster-version "latest" \\
32+
--machine-type "${argv.gcloudClusterMachineType}" \\
33+
--image-type "UBUNTU" \\
34+
--disk-type "pd-standard" \\
35+
--disk-size "50" \\
36+
--metadata disable-legacy-endpoints=true \\
37+
--scopes "https://www.googleapis.com/auth/cloud-platform" \\
38+
--num-nodes "${argv.gcloudClusterMinNodes}" \\
39+
--enable-stackdriver-kubernetes \\
40+
--enable-ip-alias \\
41+
--network "projects/$PROJECT_ID/global/networks/default" \\
42+
--subnetwork "projects/$PROJECT_ID/regions/${
43+
argv.gcloudClusterZone
44+
}/subnetworks/default" \\
45+
--default-max-pods-per-node "110" \\
46+
--addons HorizontalPodAutoscaling,HttpLoadBalancing \\
47+
--enable-autoupgrade \\
48+
--enable-autorepair \\${
49+
argv.gcloudClusterPreemtible ? "\n --preemtible \\" : ""
50+
}
51+
--enable-network-policy \\
52+
--enable-autoscaling \\
53+
--min-nodes "${argv.gcloudClusterMinNodes}" \\
54+
--max-nodes "${argv.gcloudClusterMaxNodes}"`;
55+
};
56+
957
export async function cli(args) {
1058
let argv = yargs(hideBin(args))
1159
.option("method", {
@@ -30,30 +78,61 @@ export async function cli(args) {
3078
type: "string",
3179
alias: "n",
3280
description: "Name for Coder subdomain",
81+
})
82+
.option("gcloud-project-id", {
83+
type: "string",
84+
})
85+
.option("gcloud-cluster-name", {
86+
type: "string",
87+
default: "coder",
88+
})
89+
.option("gcloud-cluster-zone", {
90+
type: "string",
91+
default: "us-central1-a",
92+
})
93+
.option("gcloud-cluster-machine-type", {
94+
type: "string",
95+
default: "e2-highmem-4",
96+
})
97+
.option("gcloud-cluster-preemtible", {
98+
type: "boolean",
99+
default: true,
100+
})
101+
.option("gcloud-cluster-autoscaling", {
102+
type: "boolean",
103+
default: true,
104+
})
105+
.option("gcloud-cluster-min-nodes", {
106+
type: "number",
107+
default: 1,
108+
})
109+
.option("gcloud-cluster-max-nodes", {
110+
type: "number",
111+
default: 3,
112+
})
113+
// TODO: determine better naming for this:
114+
.option("gcloud-skip-confirm-prompt", {
115+
type: "boolean",
33116
}).argv;
34117

35118
// detect if we are on google cloud :)
36119

37-
const checkGoogleCloud = await execa("/bin/sh", [
38-
// probably a silly way to do so, considering I can also ping in node
39-
// oh well. hackathon
40-
__dirname + "/../shell-helpers/detectGoogleCloud.sh",
41-
]);
120+
const checkGoogleCloud = await runHelperScript("detectGoogleCloud");
121+
42122
if (!argv.method && checkGoogleCloud && checkGoogleCloud.stdout == "true") {
43123
console.log(
44124
"Auto-detected you are on Google Cloud, so we'll deploy there 🚀\nYou can manually change this by executing with --method"
45125
);
46126
} else if (argv.method == undefined) {
47-
console.log("YEP IT IS IN he", argv.method);
48127
argv = {
49128
...argv,
50129
...(await inquirer.prompt({
51130
type: "list",
52131
name: "method",
53-
message: "Where would you like to deploy Coder",
132+
message: "Where would you like to deploy Coder?",
54133
choices: [
55134
{
56-
name: `Create a fresh Google Cloud cluster for me!`,
135+
name: `Create a fresh Google Cloud cluster for me and install Coder`,
57136
value: "gcloud",
58137
},
59138
{
@@ -65,6 +144,156 @@ export async function cli(args) {
65144
};
66145
}
67146

147+
if (argv.method == "gcloud") {
148+
// ensure gcloud-cli is installed and active
149+
150+
// TODO: add better user education on what the prereqs are
151+
console.log("Checking for prerequisites...");
152+
try {
153+
await runHelperScript("googleCloudPrereqs");
154+
console.log("✅", "You seem to have all the dependencies installed!");
155+
} catch (err) {
156+
console.log("❌", err.stderr);
157+
return;
158+
}
159+
160+
if (!argv.gcloudProjectId) {
161+
let defaultProject = false;
162+
let projects = [];
163+
164+
// try to get the default project
165+
try {
166+
const listOfProjects = await runHelperScript(
167+
"googleCloudDefaultProject"
168+
);
169+
170+
defaultProject = await runHelperScript("googleCloudDefaultProject");
171+
const projectsJson = await runHelperScript("googleCloudProjects");
172+
projects = JSON.parse(projectsJson).map((project) => {
173+
return project.projectId;
174+
});
175+
176+
// ensure we are actually fetching IDs
177+
if (projects[0] == undefined) {
178+
throw "could not read project ID";
179+
}
180+
181+
console.log("📄 Got a list of your Google Cloud projects!\n");
182+
} catch (err) {
183+
// reset projects list
184+
projects = [];
185+
186+
// TODO: ensure it is actually no biggie
187+
console.log("Ran into an error fetching your projects... No biggie 🙂");
188+
}
189+
190+
// show a select field if we found a list
191+
if (projects.length) {
192+
argv = {
193+
...argv,
194+
...(await inquirer.prompt({
195+
type: "list",
196+
name: "gcloudProjectId",
197+
default: defaultProject,
198+
message: `Google Cloud Project:`,
199+
validate: (that) => {
200+
// TODO: validate this project actually exists
201+
return that != "";
202+
},
203+
choices: projects,
204+
})),
205+
};
206+
} else
207+
argv = {
208+
...argv,
209+
...(await inquirer.prompt({
210+
type: "input",
211+
name: "gcloudProjectId",
212+
default: undefined,
213+
message: `Google Cloud Project:`,
214+
validate: (that) => {
215+
// TODO: validate this project actually exists
216+
return that != "";
217+
},
218+
choices: [
219+
{
220+
name: `Create a fresh Google Cloud cluster for me and install Coder`,
221+
value: "gcloud",
222+
},
223+
{
224+
name: "Install Coder on my current cluster (sketchy)",
225+
value: "k8s",
226+
},
227+
],
228+
})),
229+
};
230+
}
231+
232+
let gCloudCommand = generateGoogleClusterCommand(argv);
233+
234+
// TODO: impliment pricing calculations with Google API
235+
let pricing_info = "";
236+
237+
if (
238+
argv.gcloudClusterZone == "us-central1-a" &&
239+
argv.gcloudClusterMachineType == "e2-highmem-4" &&
240+
argv.gcloudClusterMinNodes == "1" &&
241+
argv.gcloudClusterMaxNodes == "3" &&
242+
argv.gcloudClusterAutoscaling &&
243+
argv.gcloudClusterPreemtible
244+
) {
245+
pricing_info =
246+
"This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." +
247+
"\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:";
248+
} else {
249+
pricing_info =
250+
"You are not using default settings. Be sure to calculate the pricing info for your cluster";
251+
}
252+
console.log(
253+
"\n💻 Your command is:",
254+
"\n------------\n",
255+
256+
gCloudCommand,
257+
"\n------------",
258+
"\n\n💵 " + pricing_info + "\n",
259+
"\t➡️ GKE Pricing: https://cloud.google.com/kubernetes-engine/pricing\n",
260+
"\t➡️ Storage pricing: https://cloud.google.com/compute/disks-image-pricing\n",
261+
"\t➡️ Machine pricing: https://cloud.google.com/compute/all-pricing\n\n",
262+
"\t➡️ or use the Google Cloud Pricing Calculator: https://cloud.google.com/products/calculator\n",
263+
"\n------------"
264+
);
265+
266+
// TODO: impliment ability to edit cluster command in the cli (wohoo)
267+
268+
if (!argv.gcloudSkipConfirmPrompt) {
269+
const runCommand = await inquirer.prompt({
270+
type: "confirm",
271+
default: true,
272+
name: "runIt",
273+
message: "Do you want to run this command?",
274+
});
275+
276+
if (!runCommand.runIt) {
277+
console.log(
278+
`\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --mode k8s" to install Coder on the cluster you manually created`
279+
);
280+
return 0;
281+
}
282+
}
283+
284+
const subprocess = execa("ping", ["google.com", "-c", "5"]);
285+
subprocess.stdout.pipe(process.stdout);
286+
const { stdout } = await subprocess;
287+
console.log("WE KNOW THE PROCESS HAS COMPLETED");
288+
289+
// execa("echo", ["unicorns"]).stdout.pipe(process.stdout);
290+
} else if (argv.method == "k8s") {
291+
console.log("coming sooon moo");
292+
} else {
293+
console.error("Error. Unknown method: " + argv.method);
294+
return;
295+
}
296+
68297
// determine which type of domain to use
69298
if (!argv.domainType) {
70299
argv = {
@@ -86,5 +315,5 @@ export async function cli(args) {
86315
})),
87316
};
88317
}
89-
console.log("epic answer dood", argv);
318+
console.log("\n\nat the end with a long argv:", Object.keys(argv).length);
90319
}

0 commit comments

Comments
 (0)