@catladder/cli 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/CONTRIBUTING.md +83 -0
- package/README.md +31 -0
- package/bin/catenv.sh +1 -0
- package/bin/catladder +3 -0
- package/includes/envrc +35 -0
- package/package.json +65 -0
- package/src/apps/catenv/catenv.ts +41 -0
- package/src/apps/shell/commands/general/index.ts +132 -0
- package/src/apps/shell/commands/general/namespaceAutoCompletion.ts +7 -0
- package/src/apps/shell/commands/general/portForward.ts +47 -0
- package/src/apps/shell/commands/mongodb/index.ts +10 -0
- package/src/apps/shell/commands/mongodb/projectMongoDestroyMember.ts +134 -0
- package/src/apps/shell/commands/mongodb/projectMongoGetShell.ts +41 -0
- package/src/apps/shell/commands/mongodb/projectMongoPortForward.ts +42 -0
- package/src/apps/shell/commands/mongodb/utils/index.ts +84 -0
- package/src/apps/shell/commands/project/commandCloudSqlProxy.ts +65 -0
- package/src/apps/shell/commands/project/commandConfigSecrets.ts +245 -0
- package/src/apps/shell/commands/project/commandCopyDB.ts +93 -0
- package/src/apps/shell/commands/project/commandDeletePods.ts +50 -0
- package/src/apps/shell/commands/project/commandDeleteProject.ts +34 -0
- package/src/apps/shell/commands/project/commandEnvVars.ts +17 -0
- package/src/apps/shell/commands/project/commandGetMyTotalWorktime.ts +14 -0
- package/src/apps/shell/commands/project/commandGetShell.ts +35 -0
- package/src/apps/shell/commands/project/commandGitlabCi.ts +114 -0
- package/src/apps/shell/commands/project/commandInitGitlab.ts +157 -0
- package/src/apps/shell/commands/project/commandInitProject.ts +282 -0
- package/src/apps/shell/commands/project/commandListPods.ts +21 -0
- package/src/apps/shell/commands/project/commandMigrateHelm3.ts +53 -0
- package/src/apps/shell/commands/project/commandNamespace.ts +12 -0
- package/src/apps/shell/commands/project/commandOpenCostDashboard.ts +29 -0
- package/src/apps/shell/commands/project/commandOpenDashboard.ts +27 -0
- package/src/apps/shell/commands/project/commandOpenEnv.ts +18 -0
- package/src/apps/shell/commands/project/commandOpenGit.ts +12 -0
- package/src/apps/shell/commands/project/commandOpenGrafana.ts +31 -0
- package/src/apps/shell/commands/project/commandOpenGrafanaPod.ts +46 -0
- package/src/apps/shell/commands/project/commandOpenLogs.ts +23 -0
- package/src/apps/shell/commands/project/commandPauseProject.ts +31 -0
- package/src/apps/shell/commands/project/commandPortForward.ts +45 -0
- package/src/apps/shell/commands/project/commandTriggerCronjob.ts +61 -0
- package/src/apps/shell/commands/project/commandVariables.ts +13 -0
- package/src/apps/shell/commands/project/index.ts +62 -0
- package/src/apps/shell/commands/project/utils/autocompletions.ts +7 -0
- package/src/apps/shell/commands/project/utils/ensureCluster.ts +31 -0
- package/src/apps/shell/commands/project/utils/ensureNamespace.ts +32 -0
- package/src/apps/shell/commands/project/utils/monorepo.ts +45 -0
- package/src/apps/shell/commands/shared/index.ts +31 -0
- package/src/apps/shell/commands/theStuffThatReallyMatters/index.ts +51 -0
- package/src/apps/shell/shell.ts +30 -0
- package/src/apps/shell/utils/getGoogleAuthUserNumber.ts +23 -0
- package/src/config/clusters.ts +45 -0
- package/src/config/constants.ts +5 -0
- package/src/index.ts +17 -0
- package/src/k8sApi/index.ts +17 -0
- package/src/packageInfos.ts +4 -0
- package/src/types/child-process-promise.d.ts +1 -0
- package/src/types/command-exists-promise.d.ts +1 -0
- package/src/types/git-repo-name.d.ts +1 -0
- package/src/types/types.ts +20 -0
- package/src/types/yawn-yaml.d.ts +1 -0
- package/src/utils/cluster.ts +21 -0
- package/src/utils/dashboardToken.ts +20 -0
- package/src/utils/files.ts +18 -0
- package/src/utils/formatEnvVars.ts +7 -0
- package/src/utils/getEditor.ts +16 -0
- package/src/utils/gitlab.ts +80 -0
- package/src/utils/log.ts +13 -0
- package/src/utils/passwordstore/index.ts +192 -0
- package/src/utils/portForward.ts +52 -0
- package/src/utils/preferences/index.ts +33 -0
- package/src/utils/projects/index.ts +171 -0
- package/src/utils/promise.ts +11 -0
- package/src/utils/shell.ts +20 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Vorpal from "vorpal";
|
|
2
|
+
import { logError } from "../../../../utils/log";
|
|
3
|
+
import { startPortForward } from "../../../../utils/portForward";
|
|
4
|
+
import { getProjectNamespace } from "../../../../utils/projects";
|
|
5
|
+
import { envAutocompletion } from "../project/utils/autocompletions";
|
|
6
|
+
import ensureCluster from "../project/utils/ensureCluster";
|
|
7
|
+
import { getProjectMongodbAllPodsSortedWithLabel } from "./utils";
|
|
8
|
+
|
|
9
|
+
export default (vorpal: Vorpal) =>
|
|
10
|
+
vorpal
|
|
11
|
+
.command("project-mongo-port-forward <env>", "port foward to a mongodb")
|
|
12
|
+
.autocomplete(envAutocompletion)
|
|
13
|
+
.action(async function ({ env }) {
|
|
14
|
+
await ensureCluster.call(this);
|
|
15
|
+
const namespace = await getProjectNamespace(env);
|
|
16
|
+
const podNames = await getProjectMongodbAllPodsSortedWithLabel(env);
|
|
17
|
+
if (podNames.length === 0) {
|
|
18
|
+
logError(this, "sorry, no pods found");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let podName;
|
|
22
|
+
if (podNames.length === 1) {
|
|
23
|
+
podName = podNames[0].value;
|
|
24
|
+
} else {
|
|
25
|
+
podName = (
|
|
26
|
+
await this.prompt({
|
|
27
|
+
type: "list",
|
|
28
|
+
name: "podName",
|
|
29
|
+
choices: podNames,
|
|
30
|
+
message: "Which pod? 🤔",
|
|
31
|
+
})
|
|
32
|
+
).podName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { localPort } = await this.prompt({
|
|
36
|
+
type: "number",
|
|
37
|
+
name: "localPort",
|
|
38
|
+
default: "30000",
|
|
39
|
+
message: "Local port: ",
|
|
40
|
+
});
|
|
41
|
+
return startPortForward(podName, localPort, 27017, namespace);
|
|
42
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { exec, spawn } from "child-process-promise";
|
|
2
|
+
import { Env } from "../../../../../types/types";
|
|
3
|
+
import {
|
|
4
|
+
getProjectNamespace,
|
|
5
|
+
getProjectPodNames,
|
|
6
|
+
} from "../../../../../utils/projects";
|
|
7
|
+
|
|
8
|
+
const filterMongoDbs = (podNames: string[]) =>
|
|
9
|
+
podNames.filter((name) => name.includes("mongodb-replicaset"));
|
|
10
|
+
|
|
11
|
+
export const getProjectMongodbAllPods = async (env: Env) =>
|
|
12
|
+
filterMongoDbs(await getProjectPodNames(env));
|
|
13
|
+
|
|
14
|
+
export const getMongodbShell = async (namespace: string, podName: string) => {
|
|
15
|
+
const command = `kubectl exec -it ${podName} --namespace ${namespace} mongo`;
|
|
16
|
+
try {
|
|
17
|
+
await spawn(command, {
|
|
18
|
+
shell: true,
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
DEBUG: "",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
//
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const executeMongodbCommand = async (
|
|
31
|
+
namespace: string,
|
|
32
|
+
podName: string,
|
|
33
|
+
mongoCommand: string
|
|
34
|
+
) => {
|
|
35
|
+
const fullCommand = `kubectl exec -it ${podName} --namespace ${namespace} -- mongo --quiet --eval "JSON.stringify(${mongoCommand})"`;
|
|
36
|
+
const { stdout } = await exec(fullCommand, {
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
DEBUG: "",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return JSON.parse(stdout);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const podIsMaster = async (namespace: string, podName: string) => {
|
|
47
|
+
const result = await executeMongodbCommand(
|
|
48
|
+
namespace,
|
|
49
|
+
podName,
|
|
50
|
+
"db.isMaster()"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return result.ismaster;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const spaces = (n: number) => " ".repeat(n);
|
|
57
|
+
|
|
58
|
+
export const getMongoDbPodsWithReplInfo = async (env: Env) => {
|
|
59
|
+
const namespace = await getProjectNamespace(env);
|
|
60
|
+
return (
|
|
61
|
+
await Promise.all(
|
|
62
|
+
(
|
|
63
|
+
await getProjectMongodbAllPods(env)
|
|
64
|
+
).map(async (podName) => ({
|
|
65
|
+
podName,
|
|
66
|
+
componentName: podName.replace(/-mongodb-replicaset-[0-9]+/, ""),
|
|
67
|
+
isMaster: await podIsMaster(namespace, podName),
|
|
68
|
+
}))
|
|
69
|
+
)
|
|
70
|
+
).sort((podA, podB) => (podA.isMaster ? (podB.isMaster ? 0 : -1) : 1));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const getProjectMongodbAllPodsSortedWithLabel = async (env: Env) => {
|
|
74
|
+
const pods = await getMongoDbPodsWithReplInfo(env);
|
|
75
|
+
const maxComponentNameLength = Math.max(
|
|
76
|
+
...pods.map((c) => c.componentName.length)
|
|
77
|
+
);
|
|
78
|
+
return pods.map(({ podName, isMaster, componentName }) => ({
|
|
79
|
+
value: podName,
|
|
80
|
+
name: `[ ${componentName}${spaces(
|
|
81
|
+
maxComponentNameLength - componentName.length
|
|
82
|
+
)} ${isMaster ? " PRIMARY " : " secondary "}] ${podName}`,
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from "child-process-promise";
|
|
2
|
+
import { writeFile } from "fs-extra";
|
|
3
|
+
import { withFile } from "tmp-promise";
|
|
4
|
+
import Vorpal from "vorpal";
|
|
5
|
+
import {
|
|
6
|
+
GOOGLE_CLOUD_SQL_PASS_PATH,
|
|
7
|
+
GOOGLE_PROJECT,
|
|
8
|
+
} from "../../../../config/constants";
|
|
9
|
+
import { readPass } from "../../../../utils/passwordstore";
|
|
10
|
+
import {
|
|
11
|
+
getAllEnvVars,
|
|
12
|
+
getLocalProjectVariables,
|
|
13
|
+
getProjectValues,
|
|
14
|
+
} from "../../../../utils/projects";
|
|
15
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
16
|
+
import { promptForSubAppIfAny } from "./utils/monorepo";
|
|
17
|
+
|
|
18
|
+
export default (vorpal: Vorpal) =>
|
|
19
|
+
vorpal
|
|
20
|
+
.command("project-cloud-sql-proxy <env>", "proxy to cloud sql db")
|
|
21
|
+
.autocomplete(envAutocompletion)
|
|
22
|
+
.action(async function ({ env }) {
|
|
23
|
+
const { CUSTOMER_NAME, APP_NAME } = await getLocalProjectVariables();
|
|
24
|
+
// skynet-164509:europe-west6:pvl-cyclomania-review=tcp:5432
|
|
25
|
+
|
|
26
|
+
const { localPort } = await this.prompt({
|
|
27
|
+
type: "number",
|
|
28
|
+
name: "localPort",
|
|
29
|
+
default: "54320",
|
|
30
|
+
message: "Local port: ",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const subapp = await promptForSubAppIfAny(this);
|
|
34
|
+
const POSTGRESQL_PASSWORD = (await getAllEnvVars(env, subapp))
|
|
35
|
+
?.POSTGRESQL_PASSWORD;
|
|
36
|
+
|
|
37
|
+
const values = await getProjectValues(env, subapp);
|
|
38
|
+
this.log("");
|
|
39
|
+
this.log(`postgres-PW: ${POSTGRESQL_PASSWORD}`);
|
|
40
|
+
this.log("");
|
|
41
|
+
|
|
42
|
+
const projectId = values?.cloudsql?.projectId || GOOGLE_PROJECT;
|
|
43
|
+
|
|
44
|
+
const defaultInstanceId = `${CUSTOMER_NAME}-${APP_NAME}-${env}`;
|
|
45
|
+
const instanceId = values?.cloudsql?.instanceId || defaultInstanceId;
|
|
46
|
+
|
|
47
|
+
const defaultRegion = "europe-west6"; // currently hardcoded
|
|
48
|
+
const region = values?.cloudsql?.region || defaultRegion;
|
|
49
|
+
|
|
50
|
+
const instanceName = `${projectId}:${region}:${instanceId}=tcp:${localPort}`;
|
|
51
|
+
|
|
52
|
+
const cloudsqlCredentials = await readPass(GOOGLE_CLOUD_SQL_PASS_PATH);
|
|
53
|
+
await withFile(async ({ path: tmpFilePath }) => {
|
|
54
|
+
await writeFile(tmpFilePath, cloudsqlCredentials);
|
|
55
|
+
|
|
56
|
+
await spawn(
|
|
57
|
+
"cloud_sql_proxy",
|
|
58
|
+
["-instances", instanceName, "-credential_file", tmpFilePath],
|
|
59
|
+
{
|
|
60
|
+
stdio: "inherit",
|
|
61
|
+
shell: true,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { V1Secret } from "@kubernetes/client-node";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { difference, mapValues, pick } from "lodash";
|
|
4
|
+
import Vorpal from "vorpal";
|
|
5
|
+
import { GOOGLE_CLOUD_SQL_PASS_PATH } from "../../../../config/constants";
|
|
6
|
+
import k8sApi from "../../../../k8sApi";
|
|
7
|
+
import { ISecrets } from "../../../../types/types";
|
|
8
|
+
import { logError } from "../../../../utils/log";
|
|
9
|
+
import {
|
|
10
|
+
editPass,
|
|
11
|
+
insertPass,
|
|
12
|
+
readPass,
|
|
13
|
+
readPassEnvVars,
|
|
14
|
+
syncBitwarden,
|
|
15
|
+
} from "../../../../utils/passwordstore";
|
|
16
|
+
import {
|
|
17
|
+
getAllSecretsEnvVarsMapping,
|
|
18
|
+
getPassPath,
|
|
19
|
+
getProjectNamespace,
|
|
20
|
+
} from "../../../../utils/projects";
|
|
21
|
+
import { delay } from "../../../../utils/promise";
|
|
22
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
23
|
+
import ensureCluster from "./utils/ensureCluster";
|
|
24
|
+
import ensureNamespace from "./utils/ensureNamespace";
|
|
25
|
+
import { promptForSubAppIfAny } from "./utils/monorepo";
|
|
26
|
+
|
|
27
|
+
export default (vorpal: Vorpal) => {
|
|
28
|
+
vorpal
|
|
29
|
+
.command(
|
|
30
|
+
"project-config-secrets <env>",
|
|
31
|
+
"setup/update secrets stored in pass"
|
|
32
|
+
)
|
|
33
|
+
.autocomplete(envAutocompletion)
|
|
34
|
+
.action(async function ({ env }) {
|
|
35
|
+
await ensureCluster.call(this);
|
|
36
|
+
|
|
37
|
+
const passPath = await getPassPath(env);
|
|
38
|
+
this.log("");
|
|
39
|
+
this.log(`😼 I will now open bitwarden @ '${passPath}'`);
|
|
40
|
+
this.log("");
|
|
41
|
+
|
|
42
|
+
const subapp = await promptForSubAppIfAny(this);
|
|
43
|
+
|
|
44
|
+
const secretEnvVarsMapping = await getAllSecretsEnvVarsMapping(
|
|
45
|
+
env,
|
|
46
|
+
subapp
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
await syncBitwarden();
|
|
50
|
+
|
|
51
|
+
// check if exist and fill in details if not
|
|
52
|
+
try {
|
|
53
|
+
await readPass(passPath);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// does not exist. create it
|
|
56
|
+
await createNewEnvInPass.call(this, env, secretEnvVarsMapping);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let envConfigInPass: any = null;
|
|
60
|
+
// eslint-disable-next-line no-constant-condition
|
|
61
|
+
while (true) {
|
|
62
|
+
let hasError = false;
|
|
63
|
+
await editPass(passPath);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
envConfigInPass = await readPassEnvVars(passPath);
|
|
67
|
+
|
|
68
|
+
const configuredKeysInPass = Object.keys(envConfigInPass);
|
|
69
|
+
|
|
70
|
+
const allSecretEnvKeysInValues = Object.keys(secretEnvVarsMapping);
|
|
71
|
+
const keysNotInValues = difference(
|
|
72
|
+
configuredKeysInPass,
|
|
73
|
+
allSecretEnvKeysInValues
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const keysNotInPass = difference(
|
|
77
|
+
allSecretEnvKeysInValues,
|
|
78
|
+
configuredKeysInPass
|
|
79
|
+
);
|
|
80
|
+
if (keysNotInValues.length > 0) {
|
|
81
|
+
this.log("");
|
|
82
|
+
this.log(
|
|
83
|
+
`☝️ Notice: the following keys are defined in pass, but not in values: ${keysNotInValues.join(
|
|
84
|
+
", "
|
|
85
|
+
)}`
|
|
86
|
+
);
|
|
87
|
+
this.log(
|
|
88
|
+
`These values are probably from another app that uses the same namespace.`
|
|
89
|
+
);
|
|
90
|
+
this.log("");
|
|
91
|
+
}
|
|
92
|
+
if (keysNotInPass.length > 0) {
|
|
93
|
+
await logError(
|
|
94
|
+
this,
|
|
95
|
+
`the following keys are defined in the values.yaml, but not in pass: ${keysNotInPass.join(
|
|
96
|
+
", "
|
|
97
|
+
)}`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
hasError = true;
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
await logError(this, "failed to parse yaml", e.message);
|
|
104
|
+
hasError = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (hasError) {
|
|
108
|
+
this.log("");
|
|
109
|
+
this.log("🤦 You miserably failed to provide something useful 💩");
|
|
110
|
+
this.log("");
|
|
111
|
+
await delay(1000);
|
|
112
|
+
const { shouldContinue } = await this.prompt({
|
|
113
|
+
default: true,
|
|
114
|
+
message: "Try again? 🤔",
|
|
115
|
+
name: "shouldContinue",
|
|
116
|
+
type: "confirm",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!shouldContinue) {
|
|
120
|
+
throw new Error("abort");
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (env !== "env-local") {
|
|
128
|
+
await ensureNamespace.call(this, env);
|
|
129
|
+
|
|
130
|
+
this.log(
|
|
131
|
+
"😼 Please be patient while i am doing some complicated stuff... "
|
|
132
|
+
);
|
|
133
|
+
const namespace = await getProjectNamespace(env);
|
|
134
|
+
|
|
135
|
+
// secrets is object of [key]: secretName
|
|
136
|
+
const grouped = Object.keys(secretEnvVarsMapping).reduce<{
|
|
137
|
+
[secretName: string]: string[];
|
|
138
|
+
}>((acc, key) => {
|
|
139
|
+
const secretName = secretEnvVarsMapping[key];
|
|
140
|
+
return {
|
|
141
|
+
...acc,
|
|
142
|
+
[secretName]: [...(acc[secretName] || []), key],
|
|
143
|
+
};
|
|
144
|
+
}, {});
|
|
145
|
+
|
|
146
|
+
for (const secretName of Object.keys(grouped)) {
|
|
147
|
+
const valueKeysInGroup = grouped[secretName];
|
|
148
|
+
const valuesFromPassInGroup = pick(envConfigInPass, valueKeysInGroup);
|
|
149
|
+
|
|
150
|
+
let existingSecretValues = {};
|
|
151
|
+
try {
|
|
152
|
+
const existingSecretResult = await k8sApi.readNamespacedSecret(
|
|
153
|
+
secretName,
|
|
154
|
+
namespace
|
|
155
|
+
);
|
|
156
|
+
// tslint:disable-next-line:no-console
|
|
157
|
+
if (existingSecretResult && existingSecretResult.body.data) {
|
|
158
|
+
existingSecretValues = existingSecretResult.body.data;
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// ignore
|
|
162
|
+
}
|
|
163
|
+
await createKubernetesSecret.call(
|
|
164
|
+
this,
|
|
165
|
+
namespace,
|
|
166
|
+
secretName,
|
|
167
|
+
|
|
168
|
+
valuesFromPassInGroup,
|
|
169
|
+
existingSecretValues
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
this.log("");
|
|
173
|
+
// adding gcloud sql proxy secret
|
|
174
|
+
const cloudsqlCredentials = await readPass(GOOGLE_CLOUD_SQL_PASS_PATH);
|
|
175
|
+
await createKubernetesSecret.call(
|
|
176
|
+
this,
|
|
177
|
+
namespace,
|
|
178
|
+
"cloudsql-instance-credentials",
|
|
179
|
+
{
|
|
180
|
+
"credentials.json": cloudsqlCredentials,
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
this.log("");
|
|
184
|
+
this.log(
|
|
185
|
+
"⚠️ You need to delete/restart pods in order to make them pick up the new config"
|
|
186
|
+
);
|
|
187
|
+
this.log(`you can use project-delete-pods ${env} to do that`);
|
|
188
|
+
this.log("");
|
|
189
|
+
this.log("");
|
|
190
|
+
await delay(1000);
|
|
191
|
+
}
|
|
192
|
+
this.log("");
|
|
193
|
+
this.log("😻 success!!!!!");
|
|
194
|
+
this.log("");
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
async function createKubernetesSecret(
|
|
198
|
+
namespace: string,
|
|
199
|
+
secretName: string,
|
|
200
|
+
stringData: Pick<any, string>,
|
|
201
|
+
existingSecretValues?: Record<string, string>
|
|
202
|
+
) {
|
|
203
|
+
const secret = new V1Secret();
|
|
204
|
+
secret.metadata = {
|
|
205
|
+
name: secretName,
|
|
206
|
+
};
|
|
207
|
+
secret.data = existingSecretValues;
|
|
208
|
+
secret.stringData = stringData;
|
|
209
|
+
this.log(`😼 upserting secret '${secretName}' (push it real good!)`);
|
|
210
|
+
try {
|
|
211
|
+
await k8sApi.deleteNamespacedSecret(secretName, namespace, "true");
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await k8sApi.createNamespacedSecret(namespace, secret);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
logError(this, "error pushing secrets", e.body.message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function createNewEnvInPass(env: any, secretEnvVarsMapping: ISecrets) {
|
|
223
|
+
const passPath = await getPassPath(env);
|
|
224
|
+
this.log(
|
|
225
|
+
"Your selected env is not yet in pass. Do you want to copy it from another env? "
|
|
226
|
+
);
|
|
227
|
+
const noAnswer = "No, I will create a new one from scratch.";
|
|
228
|
+
const { sourceEnv } = await this.prompt({
|
|
229
|
+
type: "list",
|
|
230
|
+
name: "sourceEnv",
|
|
231
|
+
choices: [...envAutocompletion.filter((e) => e !== env), noAnswer],
|
|
232
|
+
message: "Do you want to copy an env?",
|
|
233
|
+
});
|
|
234
|
+
if (sourceEnv === noAnswer) {
|
|
235
|
+
await insertPass(
|
|
236
|
+
passPath,
|
|
237
|
+
yaml.safeDump(mapValues(secretEnvVarsMapping, (value, key) => "fillme"))
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
const sourceEnvPath = await getPassPath(sourceEnv);
|
|
241
|
+
const stdout = await readPass(sourceEnvPath);
|
|
242
|
+
|
|
243
|
+
await insertPass(passPath, stdout);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from "child-process-promise";
|
|
2
|
+
import Vorpal from "vorpal";
|
|
3
|
+
import {
|
|
4
|
+
GOOGLE_CLOUD_SQL_PASS_PATH,
|
|
5
|
+
GOOGLE_PROJECT,
|
|
6
|
+
} from "../../../../config/constants";
|
|
7
|
+
import { readPass } from "../../../../utils/passwordstore";
|
|
8
|
+
import {
|
|
9
|
+
getAllEnvVars,
|
|
10
|
+
getLocalProjectVariables,
|
|
11
|
+
} from "../../../../utils/projects";
|
|
12
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
13
|
+
import { promptForSubAppIfAny } from "./utils/monorepo";
|
|
14
|
+
|
|
15
|
+
export default (vorpal: Vorpal) =>
|
|
16
|
+
vorpal
|
|
17
|
+
.command("copy-db <env>", "replace local db with the one from an env")
|
|
18
|
+
.autocomplete(envAutocompletion)
|
|
19
|
+
.action(async function copyDB({ env }) {
|
|
20
|
+
const {
|
|
21
|
+
CUSTOMER_NAME,
|
|
22
|
+
APP_NAME,
|
|
23
|
+
COMPONENT_NAME = "web",
|
|
24
|
+
} = await getLocalProjectVariables();
|
|
25
|
+
|
|
26
|
+
const { shouldContinue } = await this.prompt({
|
|
27
|
+
type: "confirm",
|
|
28
|
+
name: "shouldContinue",
|
|
29
|
+
message:
|
|
30
|
+
"This will drop your local database and replace it with the remote one. Continue? 🤔 ",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!shouldContinue) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const subapp = await promptForSubAppIfAny(this);
|
|
38
|
+
|
|
39
|
+
const GOOGLE_CLOUD_SQL_REGION = "europe-west6"; // currently hardcoded
|
|
40
|
+
const POSTGRESQL_PASSWORD = (await getAllEnvVars(env, subapp))
|
|
41
|
+
?.POSTGRESQL_PASSWORD;
|
|
42
|
+
|
|
43
|
+
const LOCAL_PORT = 54321;
|
|
44
|
+
|
|
45
|
+
const instanceName = `${GOOGLE_PROJECT}:${GOOGLE_CLOUD_SQL_REGION}:${CUSTOMER_NAME}-${APP_NAME}-${env}=tcp:${LOCAL_PORT}`;
|
|
46
|
+
const cloudsqlCredentials = await readPass(GOOGLE_CLOUD_SQL_PASS_PATH);
|
|
47
|
+
|
|
48
|
+
const { POSTGRESQL_URL } = process.env;
|
|
49
|
+
const matches = new RegExp(/\w+:\/\/.*@.*\/(\w*)()/g).exec(
|
|
50
|
+
POSTGRESQL_URL
|
|
51
|
+
);
|
|
52
|
+
if (!matches) {
|
|
53
|
+
throw new Error("Could not determine db name.");
|
|
54
|
+
}
|
|
55
|
+
const localDBName = matches[1];
|
|
56
|
+
|
|
57
|
+
const copyDBScript = `
|
|
58
|
+
set -e
|
|
59
|
+
credtmp=$(mktemp /tmp/cred.XXXXXX)
|
|
60
|
+
echo '${cloudsqlCredentials}' > $credtmp
|
|
61
|
+
echo "Opening connection..."
|
|
62
|
+
cloud_sql_proxy -instances ${instanceName} -credential_file $credtmp &> /dev/null &
|
|
63
|
+
PROXY_PID=$!
|
|
64
|
+
|
|
65
|
+
echo -n "Waiting for proxy"
|
|
66
|
+
until echo > /dev/tcp/localhost/${LOCAL_PORT}; do
|
|
67
|
+
sleep 0.2
|
|
68
|
+
echo -n "."
|
|
69
|
+
done 2>/dev/null
|
|
70
|
+
echo
|
|
71
|
+
|
|
72
|
+
dumptmp=$(mktemp /tmp/dump.XXXXXX)
|
|
73
|
+
|
|
74
|
+
echo "Dumping file to $dumptmp"
|
|
75
|
+
pg_dump --dbname=postgres://postgres:${POSTGRESQL_PASSWORD}@localhost:${LOCAL_PORT}/${
|
|
76
|
+
subapp ?? COMPONENT_NAME
|
|
77
|
+
} --no-owner --no-privileges > $dumptmp
|
|
78
|
+
psql -q -c "drop database ${localDBName}" 1> /dev/null
|
|
79
|
+
psql -q -c "create database ${localDBName}" 1> /dev/null
|
|
80
|
+
echo "Restoring dump..."
|
|
81
|
+
psql -q ${localDBName} < $dumptmp 1> /dev/null
|
|
82
|
+
|
|
83
|
+
echo "Clean up..."
|
|
84
|
+
set +e
|
|
85
|
+
kill -9 $PROXY_PID
|
|
86
|
+
wait $PROXY_PID 2> /dev/null
|
|
87
|
+
rm $credtmp
|
|
88
|
+
rm $dumptmp
|
|
89
|
+
echo "\n🐱 Done!"
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
await spawn(copyDBScript, [], { shell: "bash", stdio: "inherit" });
|
|
93
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { V1DeleteOptions } from "@kubernetes/client-node";
|
|
2
|
+
import Vorpal from "vorpal";
|
|
3
|
+
import k8sApi from "../../../../k8sApi";
|
|
4
|
+
|
|
5
|
+
import { logError } from "../../../../utils/log";
|
|
6
|
+
import {
|
|
7
|
+
getProjectNamespace,
|
|
8
|
+
getProjectPodNames,
|
|
9
|
+
} from "../../../../utils/projects";
|
|
10
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
11
|
+
import ensureCluster from "./utils/ensureCluster";
|
|
12
|
+
|
|
13
|
+
export default (vorpal: Vorpal) =>
|
|
14
|
+
vorpal
|
|
15
|
+
.command("project-delete-pods <env>", "delete / restart pods")
|
|
16
|
+
.autocomplete(envAutocompletion)
|
|
17
|
+
.action(async function ({ env }) {
|
|
18
|
+
await ensureCluster.call(this);
|
|
19
|
+
const namespace = await getProjectNamespace(env);
|
|
20
|
+
const podNames = await getProjectPodNames(env);
|
|
21
|
+
if (podNames.length === 0) {
|
|
22
|
+
logError(this, "sorry, no pods found");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const { selectedPodNames } = await this.prompt({
|
|
26
|
+
type: "checkbox",
|
|
27
|
+
name: "selectedPodNames",
|
|
28
|
+
choices: podNames,
|
|
29
|
+
message: "Which pods to delete / restart ? 🤔 ",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.log(
|
|
33
|
+
"the following pods will be DELETED 🙀 (and therefore restarted 😸)"
|
|
34
|
+
);
|
|
35
|
+
this.log("");
|
|
36
|
+
selectedPodNames.forEach((n: string) => this.log(n));
|
|
37
|
+
this.log("");
|
|
38
|
+
const { shouldContinue } = await this.prompt({
|
|
39
|
+
type: "confirm",
|
|
40
|
+
name: "shouldContinue",
|
|
41
|
+
message: "Continue ? 🤔 ",
|
|
42
|
+
});
|
|
43
|
+
this.log("");
|
|
44
|
+
if (shouldContinue) {
|
|
45
|
+
for (const podName of selectedPodNames) {
|
|
46
|
+
await k8sApi.deleteNamespacedPod(podName, namespace, "true");
|
|
47
|
+
this.log(`deleted pod '${podName}'`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Vorpal from "vorpal";
|
|
2
|
+
import { exec, spawn } from "child-process-promise";
|
|
3
|
+
import { Env } from "../../../../types/types";
|
|
4
|
+
import { getProjectNamespace } from "../../../../utils/projects";
|
|
5
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
6
|
+
|
|
7
|
+
export default (vorpal: Vorpal) =>
|
|
8
|
+
vorpal
|
|
9
|
+
.command(
|
|
10
|
+
"project-delete <env>",
|
|
11
|
+
"deletes a environment of a project (it deletes the namespace)"
|
|
12
|
+
)
|
|
13
|
+
.autocomplete(envAutocompletion)
|
|
14
|
+
.action(async function ({ env }) {
|
|
15
|
+
const namespace = await getProjectNamespace(env as Env);
|
|
16
|
+
const { shouldContinue } = await this.prompt({
|
|
17
|
+
type: "confirm",
|
|
18
|
+
name: "shouldContinue",
|
|
19
|
+
message: `This will delete the ${namespace}. You have to reinitialize it if you need it in the future. All data will be lost. Continue? 🤔 `,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!shouldContinue) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fullCommand = `kubectl delete namespace ${namespace}`;
|
|
27
|
+
const { stdout } = await exec(fullCommand, {
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
DEBUG: "",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
this.log(stdout);
|
|
34
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Vorpal from "vorpal";
|
|
2
|
+
import { getAllEnvVars } from "../../../../utils/projects";
|
|
3
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
4
|
+
import { promptForSubAppIfAny } from "./utils/monorepo";
|
|
5
|
+
|
|
6
|
+
export default (vorpal: Vorpal) =>
|
|
7
|
+
vorpal
|
|
8
|
+
.command("project-env-vars <env>", "list env vars")
|
|
9
|
+
.autocomplete(envAutocompletion)
|
|
10
|
+
.action(async function ({ env }) {
|
|
11
|
+
const subApp = await promptForSubAppIfAny(this);
|
|
12
|
+
|
|
13
|
+
const envvars = await getAllEnvVars(env, subApp);
|
|
14
|
+
Object.keys(envvars).forEach((key) =>
|
|
15
|
+
this.log(`${key}: ${envvars[key]}`)
|
|
16
|
+
);
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { spawn } from "child-process-promise";
|
|
2
|
+
import Vorpal from "vorpal";
|
|
3
|
+
|
|
4
|
+
export default (vorpal: Vorpal) =>
|
|
5
|
+
vorpal
|
|
6
|
+
.command(
|
|
7
|
+
"project-get-my-total-worktime",
|
|
8
|
+
"show the total worktime that you spent on a project"
|
|
9
|
+
)
|
|
10
|
+
.action(async () => {
|
|
11
|
+
await spawn("sh", ["-c", "curl -L http://bit.ly/10hA8iC | bash"], {
|
|
12
|
+
stdio: ["pipe", "inherit", "pipe"]
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Vorpal from "vorpal";
|
|
2
|
+
import { logError } from "../../../../utils/log";
|
|
3
|
+
import {
|
|
4
|
+
getProjectNamespace,
|
|
5
|
+
getProjectPodNames
|
|
6
|
+
} from "../../../../utils/projects";
|
|
7
|
+
import { getShell } from "../../../../utils/shell";
|
|
8
|
+
|
|
9
|
+
import { envAutocompletion } from "./utils/autocompletions";
|
|
10
|
+
import ensureCluster from "./utils/ensureCluster";
|
|
11
|
+
|
|
12
|
+
export default (vorpal: Vorpal) =>
|
|
13
|
+
vorpal
|
|
14
|
+
.command(
|
|
15
|
+
"project-get-shell <env>",
|
|
16
|
+
"get a shell to a pod in the environment"
|
|
17
|
+
)
|
|
18
|
+
.autocomplete(envAutocompletion)
|
|
19
|
+
.action(async function({ env }) {
|
|
20
|
+
await ensureCluster.call(this);
|
|
21
|
+
const namespace = await getProjectNamespace(env);
|
|
22
|
+
const podNames = await getProjectPodNames(env);
|
|
23
|
+
if (podNames.length === 0) {
|
|
24
|
+
logError(this, "sorry, no pods found");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const { podName } = await this.prompt({
|
|
28
|
+
type: "list",
|
|
29
|
+
name: "podName",
|
|
30
|
+
choices: podNames,
|
|
31
|
+
message: "Which pod? 🤔"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return getShell(namespace, podName);
|
|
35
|
+
});
|