@catladder/cli 1.5.8 → 1.9.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/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "catladder": "./bin/catladder"
17
17
  },
18
18
  "dependencies": {
19
- "@catladder/pipeline": "1.5.8",
19
+ "@catladder/pipeline": "1.9.0",
20
20
  "@kubernetes/client-node": "^0.16.2",
21
21
  "child-process-promise": "^2.2.1",
22
22
  "command-exists-promise": "^2.0.2",
@@ -55,5 +55,5 @@
55
55
  "eslint": "^8.7.0",
56
56
  "typescript": "^4.5.4"
57
57
  },
58
- "version": "1.5.8"
58
+ "version": "1.9.0"
59
59
  }
@@ -37,8 +37,13 @@ export default async (vorpal: Vorpal) =>
37
37
  if (!isOfDeployType(context.componentConfig.deploy, "kubernetes")) {
38
38
  throw new Error("currently only supported for kubernetes deployment");
39
39
  }
40
+ this.log("");
40
41
  this.log(`postgres-PW: ${POSTGRESQL_PASSWORD}`);
41
42
  this.log("");
43
+ this.log(
44
+ `POSTGRESQL_URL=postgresql://postgres:${POSTGRESQL_PASSWORD}@localhost:${localPort}/${context.environment.envVars.KUBE_APP_NAME}?schema=public`
45
+ );
46
+ this.log("");
42
47
 
43
48
  const values = context.componentConfig.deploy.values;
44
49
 
@@ -4,6 +4,7 @@ import { difference } from "lodash";
4
4
  import Vorpal, { CommandInstance } from "vorpal";
5
5
  import { GOOGLE_CLOUD_SQL_PASS_PATH } from "../../../../config/constants";
6
6
  import {
7
+ getAllComponentsWithAllEnvsHierarchical,
7
8
  getEnvironment,
8
9
  getEnvVars,
9
10
  getPipelineContextByChoice,
@@ -16,20 +17,30 @@ import { hasBitwarden, readPass } from "../../../../utils/passwordstore";
16
17
  import { delay } from "../../../../utils/promise";
17
18
  import { allEnvsAndAllComponents } from "./utils/autocompletions";
18
19
 
20
+ type Vars = {
21
+ [env: string]: {
22
+ [componentName: string]: Record<string, string>;
23
+ };
24
+ };
19
25
  /* for convenience, parse json objects. that makes it easier to edit secrets that are object */
20
- const resolveJson = (v: Record<string, Record<string, string>>) =>
26
+ const resolveJson = (v: Vars) =>
21
27
  Object.fromEntries(
22
- Object.entries(v).map(([c, secrets]) => {
28
+ Object.entries(v).map(([componentName, envs]) => {
23
29
  return [
24
- c,
30
+ componentName,
25
31
  Object.fromEntries(
26
- Object.entries(secrets).map(([key, value]) => {
27
- try {
28
- return [key, JSON.parse(value)];
29
- } catch (e) {
30
- return [key, value];
31
- }
32
- })
32
+ Object.entries(envs).map(([env, secrets]) => [
33
+ env,
34
+ Object.fromEntries(
35
+ Object.entries(secrets).map(([key, value]) => {
36
+ try {
37
+ return [key, JSON.parse(value)];
38
+ } catch (e) {
39
+ return [key, value];
40
+ }
41
+ })
42
+ ),
43
+ ])
33
44
  ),
34
45
  ];
35
46
  })
@@ -48,14 +59,22 @@ const getEnvVarsToEdit = async (
48
59
  };
49
60
  const doItFor = async (
50
61
  instance: CommandInstance,
51
- env: string,
52
- components: string[]
62
+ envAndComponents: {
63
+ [componentName: string]: string[];
64
+ }
53
65
  ) => {
54
- let valuesToEdit: Record<string, Record<string, string>> = Object.fromEntries(
66
+ let valuesToEdit: Vars = Object.fromEntries(
55
67
  await Promise.all(
56
- components.map(async (componentName) => [
68
+ Object.entries(envAndComponents).map(async ([componentName, envs]) => [
57
69
  componentName,
58
- await getEnvVarsToEdit(instance, env, componentName),
70
+ Object.fromEntries(
71
+ await Promise.all(
72
+ envs.map(async (env) => [
73
+ env,
74
+ await getEnvVarsToEdit(instance, env, componentName),
75
+ ])
76
+ )
77
+ ),
59
78
  ])
60
79
  )
61
80
  );
@@ -64,81 +83,90 @@ const doItFor = async (
64
83
  valuesToEdit = await editAsFile(
65
84
  resolveJson(valuesToEdit),
66
85
  stripIndents`
67
- Please fill in all secrets for: ${components.join(", ")}
86
+ Please fill in all secrets for:
87
+
88
+ ${Object.entries(envAndComponents)
89
+ .map(
90
+ ([componentName, envs]) => `- ${componentName}: ${envs.join(", ")}`
91
+ )
92
+ .join("\n")}
68
93
 
69
94
  `
70
95
  );
71
96
  // check for errors
72
97
  hasErrors = false;
73
- for (const componentName of components) {
74
- const usedKeys = valuesToEdit[componentName]
75
- ? Object.keys(valuesToEdit[componentName])
76
- : [];
77
- // check whether newValues have the exact number of keys
78
- const { secretEnvVarKeys } = await getEnvironment(env, componentName);
79
- const extranous = difference(usedKeys, secretEnvVarKeys);
80
- const missing = difference(secretEnvVarKeys, usedKeys);
98
+ for (const [componentName, envs] of Object.entries(envAndComponents)) {
99
+ for (const env of envs) {
100
+ const usedKeys = valuesToEdit[componentName][env]
101
+ ? Object.keys(valuesToEdit[componentName][env])
102
+ : [];
103
+ // check whether newValues have the exact number of keys
104
+ const { secretEnvVarKeys } = await getEnvironment(env, componentName);
105
+ const extranous = difference(usedKeys, secretEnvVarKeys);
106
+ const missing = difference(secretEnvVarKeys, usedKeys);
81
107
 
82
- if (extranous.length > 0 || missing.length > 0) {
83
- instance.log("");
84
- instance.log(
85
- `😿 Oh no! There is something wrong with "${componentName}"`
86
- );
87
- instance.log("");
88
- if (extranous.length > 0) {
89
- instance.log("these secrets are not declared in the config");
90
- extranous.forEach((key) => instance.log(key));
108
+ if (extranous.length > 0 || missing.length > 0) {
91
109
  instance.log("");
92
- }
93
- if (missing.length > 0) {
94
- instance.log("these secrets have not been provided:");
95
- missing.forEach((key) => instance.log(key));
110
+ instance.log(
111
+ `😿 Oh no! There is something wrong with "${componentName}"`
112
+ );
96
113
  instance.log("");
97
- }
114
+ if (extranous.length > 0) {
115
+ instance.log("these secrets are not declared in the config");
116
+ extranous.forEach((key) => instance.log(key));
117
+ instance.log("");
118
+ }
119
+ if (missing.length > 0) {
120
+ instance.log("these secrets have not been provided:");
121
+ missing.forEach((key) => instance.log(key));
122
+ instance.log("");
123
+ }
98
124
 
99
- await delay(1000);
100
- const { shouldContinue } = await instance.prompt({
101
- default: true,
102
- message: "Try again? 🤔",
103
- name: "shouldContinue",
104
- type: "confirm",
105
- });
125
+ await delay(1000);
126
+ const { shouldContinue } = await instance.prompt({
127
+ default: true,
128
+ message: "Try again? 🤔",
129
+ name: "shouldContinue",
130
+ type: "confirm",
131
+ });
106
132
 
107
- if (!shouldContinue) {
108
- throw new Error("abort");
133
+ if (!shouldContinue) {
134
+ throw new Error("abort");
135
+ }
136
+ hasErrors = true;
109
137
  }
110
- hasErrors = true;
111
138
  }
112
139
  }
113
140
  }
141
+ for (const [componentName, envs] of Object.entries(envAndComponents)) {
142
+ for (const env of envs) {
143
+ await upsertAllVariables(
144
+ instance,
145
+ valuesToEdit[componentName][env],
146
+ env,
147
+ componentName
148
+ );
114
149
 
115
- for (const componentName of components) {
116
- await upsertAllVariables(
117
- instance,
118
- valuesToEdit[componentName],
119
- env,
120
- componentName
121
- );
122
-
123
- if (hasBitwarden()) {
124
- // add cloud sql secret if needed.
125
- // TODO: this is legacy, in the future we want to have one service account per app
150
+ if (hasBitwarden()) {
151
+ // add cloud sql secret if needed.
152
+ // TODO: this is legacy, in the future we want to have one service account per app
126
153
 
127
- const context = await getPipelineContextByChoice(env, componentName);
128
- if (
129
- context.componentConfig.deploy &&
130
- context.componentConfig.deploy.values?.cloudsql?.enabled
131
- ) {
132
- await upsertAllVariables(
133
- instance,
134
- {
135
- cloudsqlProxyCredentials: await readPass(
136
- GOOGLE_CLOUD_SQL_PASS_PATH
137
- ),
138
- },
139
- env,
140
- componentName
141
- );
154
+ const context = await getPipelineContextByChoice(env, componentName);
155
+ if (
156
+ context.componentConfig.deploy &&
157
+ context.componentConfig.deploy.values?.cloudsql?.enabled
158
+ ) {
159
+ await upsertAllVariables(
160
+ instance,
161
+ {
162
+ cloudsqlProxyCredentials: await readPass(
163
+ GOOGLE_CLOUD_SQL_PASS_PATH
164
+ ),
165
+ },
166
+ env,
167
+ componentName
168
+ );
169
+ }
142
170
  }
143
171
  }
144
172
  }
@@ -147,62 +175,31 @@ const doItFor = async (
147
175
  export default async (vorpal: Vorpal) => {
148
176
  vorpal
149
177
  .command(
150
- "project-config-secrets <envComponent>",
178
+ "project-config-secrets [envComponent]",
151
179
  "setup/update secrets stored in pass"
152
180
  )
153
181
  .autocomplete(await allEnvsAndAllComponents())
154
182
  .action(async function ({ envComponent }) {
155
- const { env, componentName } = parseChoice(envComponent);
156
-
157
- // componentName can be null. in this case, iterate over all components
158
- if (!componentName) {
159
- const components = await getProjectComponents();
160
- await doItFor(this, env, components);
161
- }
162
- if (componentName) {
163
- await doItFor(this, env, [componentName]);
164
- }
183
+ if (!envComponent) {
184
+ const allEnvAndcomponents =
185
+ await getAllComponentsWithAllEnvsHierarchical();
186
+ await doItFor(this, allEnvAndcomponents);
187
+ } else {
188
+ const { env, componentName } = parseChoice(envComponent);
165
189
 
166
- /*
167
-
168
-
169
- // adding gcloud sql proxy secret
170
- const cloudsqlCredentials = await readPass(GOOGLE_CLOUD_SQL_PASS_PATH);
171
- await createKubernetesSecret.call(
172
- this,
173
- namespace,
174
- "cloudsql-instance-credentials",
175
- {
176
- "credentials.json": cloudsqlCredentials,
177
- }
178
- );
179
- this.log("");
180
- this.log(
181
- "⚠️ You need to delete/restart pods in order to make them pick up the new config"
182
- );
183
- this.log(`you can use project-delete-pods ${env} to do that`);
184
- this.log("");
185
- this.log("");
186
- await delay(1000);
190
+ // componentName can be null. in this case, iterate over all components
191
+ if (!componentName) {
192
+ const components = await getProjectComponents();
193
+ await doItFor(
194
+ this,
195
+ Object.fromEntries(components.map((c) => [c, [env]]))
196
+ );
197
+ }
198
+ if (componentName) {
199
+ await doItFor(this, {
200
+ [componentName]: [env],
201
+ });
202
+ }
187
203
  }
188
- this.log("");
189
- this.log("😻 success!!!!!");
190
- this.log("");
191
- */
192
204
  });
193
- }; /*
194
- async function createNewEnvInPass(env: any, secretEnvVarsMapping: ISecrets) {
195
- // const passPath = await getPassPath(env);
196
- this.log(
197
- "Your selected env is not yet in pass. Do you want to copy it from another env? "
198
- );
199
- const noAnswer = "No, I will create a new one from scratch.";
200
- const { sourceEnv } = await this.prompt({
201
- type: "list",
202
- name: "sourceEnv",
203
- choices: [...(await envAndComponents()).filter((e) => e !== env), noAnswer],
204
- message: "Do you want to copy an env?",
205
- });
206
- // TODO: reimplenent
207
- }
208
- */
205
+ };
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  getFullKubernetesClusterName,
3
3
  isOfDeployType,
4
- getKubernetesNamespace,
5
4
  } from "@catladder/pipeline";
6
5
  import Vorpal from "vorpal";
7
6
  import { $ } from "zx";
8
7
  import { getAllPipelineContexts } from "../../../../config/getProjectConfig";
9
8
  import { connectToCluster } from "../../../../utils/cluster";
10
- import { upsertAllVariables } from "../../../../utils/gitlab";
9
+ import {
10
+ doGitlabRequest,
11
+ getProjectInfo,
12
+ upsertAllVariables,
13
+ } from "../../../../utils/gitlab";
11
14
  import ensureNamespace from "./utils/ensureNamespace";
15
+ import open from "open";
12
16
 
13
17
  export default async (vorpal: Vorpal) =>
14
18
  vorpal
@@ -109,69 +113,9 @@ EOF
109
113
  }
110
114
  }
111
115
 
112
- // there is a constraint, see https://git.panter.ch/catladder/catladder/-/issues/2#note_345677
113
- // any two components have to use the same custer per env, so e.g. dev:api and dev:www cannot use two different namespaces
114
- // in practise, that is often not an issue, but it might happen because the config allows it
115
- /*
116
- Object.entries(configuredClusters).forEach(([fullname, config]) =>
117
- this.log(` - ${config.cluster.name || "unknown"} (${fullname})`)
118
- );
119
- this.log("");
120
-
121
- const missingClusters = Object.fromEntries(
122
- Object.entries(configuredClusters).filter(
123
- ([c]) => !existingClusters.some((exist) => exist === c)
124
- )
125
- );
126
-
127
- this.log("");
128
- this.log("These clusters are not configured yet on gitlab:");
129
- this.log("");
130
-
131
- Object.entries(missingClusters).forEach(([fullname, config]) =>
132
- this.log(` - ${config.cluster.name || "unknown"} (${fullname})`)
116
+ const { id: projectId, web_url: projectWebUrl } = await getProjectInfo(
117
+ this
133
118
  );
134
- this.log("");
135
-
136
- for (const [fullname, config] of Object.entries(missingClusters)) {
137
- this.log(`${config.name} (${fullname})`);
138
- this.log("");
139
- const { shouldContinue } = await this.prompt({
140
- type: "confirm",
141
- name: "shouldContinue",
142
- message: "Should I add the this cluster ? 🤔 ",
143
- });
144
- this.log("");
145
- if (shouldContinue) {
146
- await connectToCluster(fullname);
147
- const { stdout: api_url } =
148
- await $`kubectl cluster-info | grep -E 'Kubernetes master|Kubernetes control plane' | awk '/http/ {print $NF}'`;
149
- const { stdout: ca_cert } =
150
- await $`kubectl get secret default-token-69xv4 -o jsonpath="{['data']['ca\.crt']}" | base64 --decode`;
151
- const { stdout: token } =
152
- await $`kubectl get secret default-token-69xv4 -o jsonpath="{['data']['token']}" | base64 --decode`;
153
- const postResult = await doGitlabRequest(
154
- this,
155
- `projects/${projectId}/clusters/user`,
156
- {
157
- name: fullname,
158
- managed: false,
159
- environment_scope: "*",
160
- platform_kubernetes_attributes: {
161
- api_url,
162
- ca_cert,
163
- token,
164
- namespace: await getProjectNamespace("prod"),
165
- },
166
- }
167
- );
168
- const { message } = postResult;
169
- if (message) {
170
- this.log(`Message from gitlab: ${message}`);
171
- }
172
- }
173
- }
174
-
175
119
  const variables = await doGitlabRequest(
176
120
  this,
177
121
  `projects/${projectId}/variables`
@@ -244,5 +188,4 @@ EOF
244
188
  ].forEach((tip) => this.log(` - ${tip}`));
245
189
  this.log("\n");
246
190
  this.log("\n");
247
- */
248
191
  });
@@ -1,6 +1,6 @@
1
1
  import { getAllEnvs, getAllEnvsInAllComponents } from "@catladder/pipeline";
2
2
  import {
3
- getAllComponentsWithAllEnvs,
3
+ getAllComponentsWithAllEnvsFlat,
4
4
  getProjectConfig,
5
5
  } from "../../../../../config/getProjectConfig";
6
6
 
@@ -13,7 +13,7 @@ export const allEnvs = async () => {
13
13
  };
14
14
 
15
15
  export const envAndComponents = async () => {
16
- const allEnvAndcomponents = await getAllComponentsWithAllEnvs();
16
+ const allEnvAndcomponents = await getAllComponentsWithAllEnvsFlat();
17
17
 
18
18
  return allEnvAndcomponents.reduce<string[]>(
19
19
  (acc, { env, componentName }) => [...acc, env + ":" + componentName],
@@ -58,21 +58,37 @@ export const getPipelineContextByChoice = async (
58
58
  const config = await getProjectConfig();
59
59
  return createContext(config, componentName, env);
60
60
  };
61
- export const getAllComponentsWithAllEnvs = async () => {
61
+ export const getAllComponentsWithAllEnvsFlat = async (): Promise<
62
+ Array<{ env: string; componentName: string }>
63
+ > => {
62
64
  const config = await getProjectConfig();
63
65
  if (!config) {
64
66
  return [];
65
67
  }
66
- return Promise.all(
67
- Object.keys(config.components).flatMap((componentName) =>
68
- getAllEnvs(config, componentName).map((env) => ({ env, componentName }))
69
- )
68
+ return Object.keys(config.components).flatMap((componentName) =>
69
+ getAllEnvs(config, componentName).map((env) => ({ env, componentName }))
70
+ );
71
+ };
72
+
73
+ export const getAllComponentsWithAllEnvsHierarchical = async (): Promise<{
74
+ [componentName: string]: string[];
75
+ }> => {
76
+ const config = await getProjectConfig();
77
+ if (!config) {
78
+ return {};
79
+ }
80
+
81
+ return Object.fromEntries(
82
+ Object.keys(config.components).map((componentName) => [
83
+ componentName,
84
+ getAllEnvs(config, componentName),
85
+ ])
70
86
  );
71
87
  };
72
88
 
73
89
  export const getAllPipelineContexts = async () => {
74
90
  return Promise.all(
75
- (await getAllComponentsWithAllEnvs())
91
+ (await getAllComponentsWithAllEnvsFlat())
76
92
  .filter((c) => c.env !== "local")
77
93
  .map(({ env, componentName }) =>
78
94
  getPipelineContextByChoice(env, componentName)