@vaharoni/devops 1.1.7 → 1.1.9
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/README.md +5 -1
- package/dist/cli/cloudrun.d.ts +11 -0
- package/dist/cli/cloudrun.d.ts.map +1 -0
- package/dist/cli/cloudrun.js +121 -0
- package/dist/cli/common.d.ts +10 -12
- package/dist/cli/common.d.ts.map +1 -1
- package/dist/cli/common.js +28 -22
- package/dist/cli/env.js +2 -2
- package/dist/cli/image.d.ts.map +1 -1
- package/dist/cli/image.js +16 -1
- package/dist/cli/prep-build.d.ts.map +1 -1
- package/dist/cli/prep-build.js +35 -6
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +15 -5
- package/dist/devops.js +3 -1
- package/dist/libs/cloudrun-helpers.d.ts +16 -0
- package/dist/libs/cloudrun-helpers.d.ts.map +1 -0
- package/dist/libs/cloudrun-helpers.js +79 -0
- package/dist/libs/config.d.ts +1 -0
- package/dist/libs/config.d.ts.map +1 -1
- package/dist/libs/config.js +4 -0
- package/dist/libs/digital-ocean/container-reg.d.ts +1 -1
- package/dist/libs/digital-ocean/container-reg.d.ts.map +1 -1
- package/dist/libs/digital-ocean/container-reg.js +7 -2
- package/dist/libs/k8s-constants.d.ts +1 -0
- package/dist/libs/k8s-constants.d.ts.map +1 -1
- package/dist/libs/k8s-constants.js +35 -10
- package/dist/libs/k8s-generate.d.ts +1 -1
- package/dist/libs/k8s-generate.d.ts.map +1 -1
- package/dist/libs/k8s-generate.js +15 -2
- package/dist/libs/k8s-secrets-manager.d.ts +2 -1
- package/dist/libs/k8s-secrets-manager.d.ts.map +1 -1
- package/dist/libs/k8s-secrets-manager.js +8 -5
- package/dist/types/index.d.ts +19 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -1
- package/package.json +1 -1
- package/src/cli/cloudrun.ts +133 -0
- package/src/cli/common.ts +46 -38
- package/src/cli/db.ts +1 -1
- package/src/cli/dml.ts +1 -1
- package/src/cli/env.ts +2 -2
- package/src/cli/exec.ts +1 -1
- package/src/cli/image.ts +15 -1
- package/src/cli/job.ts +1 -1
- package/src/cli/prep-build.ts +34 -6
- package/src/cli/redis.ts +1 -1
- package/src/cli/registry.ts +15 -5
- package/src/devops.ts +3 -1
- package/src/libs/cloudrun-helpers.ts +118 -0
- package/src/libs/config.ts +5 -0
- package/src/libs/digital-ocean/container-reg.ts +10 -2
- package/src/libs/k8s-constants.ts +36 -12
- package/src/libs/k8s-generate.ts +15 -2
- package/src/libs/k8s-secrets-manager.ts +9 -5
- package/src/target-templates/lang-variants-common/python/.devops/config/images.yaml +3 -1
- package/src/target-templates/lang-variants-common/typescript/.devops/docker-images/cloudrun.Dockerfile +31 -0
- package/src/target-templates/lang-variants-common/typescript/.github/actions/deploy-image@v1/action.yaml +4 -0
- package/src/types/index.ts +3 -1
package/src/cli/common.ts
CHANGED
@@ -4,10 +4,10 @@ import fs from "fs";
|
|
4
4
|
import { globSync } from "glob";
|
5
5
|
import { allSupportedEnvs } from "../libs/k8s-constants";
|
6
6
|
|
7
|
-
type ParsedArgs = {
|
7
|
+
type ParsedArgs<TBoolKeys extends readonly string[], TParamKeys extends readonly string[]> = {
|
8
8
|
args: string[];
|
9
9
|
argsStr: string;
|
10
|
-
options:
|
10
|
+
options: Partial<Record<TBoolKeys[number], true>> & Partial<Record<TParamKeys[number], string>>;
|
11
11
|
passthrough?: string[];
|
12
12
|
};
|
13
13
|
|
@@ -27,8 +27,8 @@ export class CLICommandParser {
|
|
27
27
|
|
28
28
|
constructor(cmdArray: string[]) {
|
29
29
|
const parsedArgs = this._separateOptions(cmdArray.filter(Boolean), {
|
30
|
-
params: ["--env"],
|
31
|
-
booleans: ["--help", "--skip-env-check"],
|
30
|
+
params: ["--env"] as const,
|
31
|
+
booleans: ["--help", "--skip-env-check"] as const,
|
32
32
|
});
|
33
33
|
const [command, ...args] = parsedArgs.args;
|
34
34
|
this.command = command;
|
@@ -67,18 +67,18 @@ export class CLICommandParser {
|
|
67
67
|
// # => { args: ['arg1'], options: { '--some-flag': true, '--in': 'workspace' } }
|
68
68
|
//
|
69
69
|
// Note that the global param --env is already extracted and can be accessed with cmd.env
|
70
|
-
parseOptions({
|
71
|
-
params
|
72
|
-
booleans
|
70
|
+
parseOptions<const TBoolKeys extends readonly string[], const TParamKeys extends readonly string[]>({
|
71
|
+
params,
|
72
|
+
booleans,
|
73
73
|
passthroughArgs = false,
|
74
74
|
}: {
|
75
75
|
/** Param is used like so: --param value */
|
76
|
-
params?:
|
76
|
+
params?: TParamKeys
|
77
77
|
/** Boolean flag is used like so: --flag */
|
78
|
-
booleans?:
|
78
|
+
booleans?: TBoolKeys;
|
79
79
|
/** Pass through args are used like so: -- arg1 arg2 */
|
80
80
|
passthroughArgs?: boolean;
|
81
|
-
} = {}): ParsedArgs {
|
81
|
+
} = {}): ParsedArgs<TBoolKeys, TParamKeys> {
|
82
82
|
return this._separateOptions(this.args, {
|
83
83
|
params,
|
84
84
|
booleans,
|
@@ -97,46 +97,54 @@ export class CLICommandParser {
|
|
97
97
|
return true;
|
98
98
|
}
|
99
99
|
|
100
|
-
_separateOptions(
|
100
|
+
_separateOptions<const TBoolKeys extends readonly string[], const TParamKeys extends readonly string[]>(
|
101
101
|
args: string[],
|
102
102
|
{
|
103
|
-
params
|
104
|
-
booleans
|
103
|
+
params,
|
104
|
+
booleans,
|
105
105
|
passthroughArgs = false,
|
106
106
|
}: {
|
107
|
-
params?:
|
108
|
-
booleans?:
|
107
|
+
params?: TParamKeys;
|
108
|
+
booleans?: TBoolKeys;
|
109
109
|
passthroughArgs?: boolean;
|
110
110
|
} = {}
|
111
|
-
): ParsedArgs {
|
112
|
-
const
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
};
|
118
|
-
const paramsLookup = Object.fromEntries(params.map((x) => [x, true]));
|
119
|
-
const booleansLookup = Object.fromEntries(booleans.map((x) => [x, true]));
|
111
|
+
): ParsedArgs<TBoolKeys, TParamKeys> {
|
112
|
+
const paramsLookup = new Set<TParamKeys[number]>(params ?? []);
|
113
|
+
const booleansLookup = new Set<TBoolKeys[number]>(booleans ?? []);
|
114
|
+
const isParam = (arg: string): arg is TParamKeys[number] => paramsLookup.has(arg);
|
115
|
+
const isBoolean = (arg: string): arg is TBoolKeys[number] => booleansLookup.has(arg);
|
116
|
+
|
120
117
|
const passthroughArgsStart = passthroughArgs ? args.indexOf("--") : -1;
|
121
|
-
|
122
|
-
|
118
|
+
// prettier-ignore
|
119
|
+
const numArgsToProcess = passthroughArgsStart === -1 ? args.length : passthroughArgsStart;
|
120
|
+
|
121
|
+
const getResPassthrough = () => {
|
122
|
+
if (!passthroughArgs || passthroughArgsStart < 0) return { passthrough: []};
|
123
|
+
return { passthrough: args.slice(passthroughArgsStart + 1) };
|
124
|
+
}
|
125
|
+
|
126
|
+
const resArgs: string[] = [];
|
127
|
+
const resParams: Partial<Record<TParamKeys[number], string>> = {};
|
128
|
+
const resOptions: Partial<Record<TBoolKeys[number], true>> = {};
|
129
|
+
|
123
130
|
for (let i = 0; i < numArgsToProcess; ++i) {
|
124
131
|
const curr = args[i];
|
125
|
-
if (
|
132
|
+
if (isParam(curr)) {
|
126
133
|
const next = args[i + 1];
|
127
|
-
|
134
|
+
resParams[curr] = next;
|
128
135
|
++i;
|
129
|
-
} else if (
|
130
|
-
|
136
|
+
} else if (isBoolean(curr)) {
|
137
|
+
resOptions[curr] = true;
|
131
138
|
} else {
|
132
|
-
|
139
|
+
resArgs.push(curr);
|
133
140
|
}
|
134
141
|
}
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
142
|
+
return {
|
143
|
+
args: resArgs,
|
144
|
+
argsStr: resArgs.join(" "),
|
145
|
+
options: { ...resOptions, ...resParams },
|
146
|
+
...getResPassthrough(),
|
147
|
+
};
|
140
148
|
}
|
141
149
|
}
|
142
150
|
|
@@ -220,10 +228,10 @@ export class CommandExecutor {
|
|
220
228
|
const envToUse = this._getProcessEnv(env);
|
221
229
|
return new Promise((resolve) => {
|
222
230
|
try {
|
223
|
-
const
|
224
|
-
const childProcess = spawn(cmd, args, {
|
231
|
+
const childProcess = spawn(fullCommand, {
|
225
232
|
stdio: "inherit",
|
226
233
|
env: envToUse,
|
234
|
+
shell: true,
|
227
235
|
});
|
228
236
|
|
229
237
|
childProcess.on("close", (code) => {
|
package/src/cli/db.ts
CHANGED
@@ -114,7 +114,7 @@ function run(cmdObj: CLICommandParser) {
|
|
114
114
|
const parsed = cmdObj.parseOptions({ params: ["-p"] });
|
115
115
|
|
116
116
|
const [command, namespace] = parsed.args;
|
117
|
-
const port = parsed.options["-p"]
|
117
|
+
const port = parsed.options["-p"];
|
118
118
|
// @ts-expect-error left as an exercise for the reader
|
119
119
|
const handler = handlers[command];
|
120
120
|
if (!handler) {
|
package/src/cli/dml.ts
CHANGED
@@ -107,7 +107,7 @@ function run(cmdObj: CLICommandParser) {
|
|
107
107
|
});
|
108
108
|
switch (parsed.args[0]) {
|
109
109
|
case "create": {
|
110
|
-
const name = parsed.options["--name"]
|
110
|
+
const name = parsed.options["--name"];
|
111
111
|
if (!name) printUsageAndExit(usage);
|
112
112
|
return createDml(name);
|
113
113
|
}
|
package/src/cli/env.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { globSync } from "glob";
|
2
2
|
import {
|
3
3
|
deleteMonorepoSecret,
|
4
|
-
|
4
|
+
getMonorepoSecretStr,
|
5
5
|
setMonorepoSecret,
|
6
6
|
} from "../libs/k8s-secrets-manager";
|
7
7
|
import { CombinedEnvValidator } from "../libs/validate-env";
|
@@ -62,7 +62,7 @@ function run(cmdObj: CLICommandParser) {
|
|
62
62
|
}
|
63
63
|
|
64
64
|
case "get": {
|
65
|
-
console.log(
|
65
|
+
console.log(getMonorepoSecretStr(cmdObj.env, rest));
|
66
66
|
break;
|
67
67
|
}
|
68
68
|
|
package/src/cli/exec.ts
CHANGED
@@ -32,7 +32,7 @@ function run(cmdObj: CLICommandParser) {
|
|
32
32
|
params: ["--in"],
|
33
33
|
booleans: ["--interactive"],
|
34
34
|
});
|
35
|
-
const workspace = parsed.options["--in"]
|
35
|
+
const workspace = parsed.options["--in"];
|
36
36
|
let executor: CommandExecutor;
|
37
37
|
if (workspace) {
|
38
38
|
const rootPath = getWorkspace(workspace).rootPath;
|
package/src/cli/image.ts
CHANGED
@@ -2,9 +2,11 @@ import { deleteImageVersion, getImageVersion, getWorkspaceScale, resetWorkspaceS
|
|
2
2
|
import { CLICommandParser, printUsageAndExit, StrongParams } from "../../src/cli/common";
|
3
3
|
import { generateImageDeployments } from "../libs/k8s-generate";
|
4
4
|
import { applyHandler } from "../libs/k8s-helpers";
|
5
|
+
import { getImageType } from "../libs/config";
|
5
6
|
|
6
7
|
const oneLiner = "Applies image-related manifests, retrieves or set the version deployed, and scales deployments of applications";
|
7
8
|
const keyExamples = `
|
9
|
+
$ devops image get type main-node
|
8
10
|
$ devops image deployment gen main-node sha --env staging
|
9
11
|
$ devops image deployment create main-node sha --env staging
|
10
12
|
$ devops image deployment delete main-node --env staging
|
@@ -21,6 +23,11 @@ const keyExamples = `
|
|
21
23
|
const usage = `
|
22
24
|
${oneLiner}
|
23
25
|
|
26
|
+
GET IMAGE TYPE
|
27
|
+
devops image get type <image-name>
|
28
|
+
|
29
|
+
Returns "k8s" or "cloudrun" depending on the image type.
|
30
|
+
|
24
31
|
GENERATING DEPLOYMENT MANIFESTS
|
25
32
|
devops image deployment gen|create|delete <image-name> <sha>
|
26
33
|
|
@@ -49,6 +56,11 @@ EXAMPLES
|
|
49
56
|
`;
|
50
57
|
|
51
58
|
const handlers = {
|
59
|
+
get: {
|
60
|
+
type: (opts: StrongParams) => {
|
61
|
+
console.log(getImageType(opts.required("image")));
|
62
|
+
},
|
63
|
+
},
|
52
64
|
deployment: {
|
53
65
|
gen: (opts: StrongParams) => {
|
54
66
|
console.log(
|
@@ -176,7 +188,9 @@ function run(cmdObj: CLICommandParser) {
|
|
176
188
|
}
|
177
189
|
|
178
190
|
function getExtraParams() {
|
179
|
-
if (command === '
|
191
|
+
if (command === 'get') {
|
192
|
+
return {};
|
193
|
+
} else if (command === 'scale') {
|
180
194
|
return subcommand === 'set' ? { workspace: param1, replicas: param2 } : { workspace: param1 };
|
181
195
|
} else {
|
182
196
|
return { sha: param1 };
|
package/src/cli/job.ts
CHANGED
@@ -65,7 +65,7 @@ function run(cmdObj: CLICommandParser) {
|
|
65
65
|
printUsageAndExit(usage);
|
66
66
|
}
|
67
67
|
|
68
|
-
const timeout = parsedArgs.options["--timeout"]
|
68
|
+
const timeout = parsedArgs.options["--timeout"];
|
69
69
|
const params = new StrongParams(usage, {
|
70
70
|
env: cmdObj.env,
|
71
71
|
image,
|
package/src/cli/prep-build.ts
CHANGED
@@ -3,8 +3,10 @@ import os from "os";
|
|
3
3
|
import path from "path";
|
4
4
|
import { CLICommandParser, printUsageAndExit } from "./common";
|
5
5
|
import { getImageData, getTemplateData } from "../libs/config";
|
6
|
-
import {
|
6
|
+
import { getMonorepoSecretStr } from "../libs/k8s-secrets-manager";
|
7
7
|
import { getImageDescendentData } from "../libs/discovery/images";
|
8
|
+
import { isLocalOrRemoteEnv } from "../libs/k8s-constants";
|
9
|
+
import chalk from "chalk";
|
8
10
|
|
9
11
|
const oneLiner =
|
10
12
|
"Copies all dependencies of an image to a temporary folder in preparation for a Docker build";
|
@@ -16,7 +18,10 @@ const usage = `
|
|
16
18
|
${oneLiner}
|
17
19
|
|
18
20
|
USAGE
|
19
|
-
devops prep-build <image>
|
21
|
+
devops prep-build <image> --env <env>
|
22
|
+
|
23
|
+
If <env> is a remote environment (e.g. staging, production), the environment variables are
|
24
|
+
fetched from the cluster and injected in case they are needed during the build process.
|
20
25
|
|
21
26
|
EXAMPLES
|
22
27
|
${keyExamples}
|
@@ -61,8 +66,10 @@ async function run(cmdObj: CLICommandParser) {
|
|
61
66
|
fs.copySync(dockerCommonPayloadPath, destFolder);
|
62
67
|
}
|
63
68
|
|
64
|
-
|
65
|
-
|
69
|
+
if (fs.existsSync(dockerImagePayloadPath)) {
|
70
|
+
console.warn(`COPYING Docker image payload`);
|
71
|
+
fs.copySync(dockerImagePayloadPath, destFolder);
|
72
|
+
}
|
66
73
|
|
67
74
|
console.warn(`COPYING .devops/config`);
|
68
75
|
fs.mkdirSync(path.join(destFolder, ".devops"));
|
@@ -71,8 +78,29 @@ async function run(cmdObj: CLICommandParser) {
|
|
71
78
|
// Create config directory. It should be deleted by the docker image so that it can be mounted as a volume when the pod is run
|
72
79
|
console.warn(`CREATING config for the build process`);
|
73
80
|
fs.mkdirSync(path.join(destFolder, "config"));
|
74
|
-
const
|
75
|
-
|
81
|
+
const destGlobalEnvPath = path.join(destFolder, "config/.env.global");
|
82
|
+
if (isLocalOrRemoteEnv(cmdObj.env) === "remote") {
|
83
|
+
const envFileData = getMonorepoSecretStr(cmdObj.env);
|
84
|
+
fs.writeFileSync(destGlobalEnvPath, envFileData);
|
85
|
+
} else {
|
86
|
+
let anyCopied = false;
|
87
|
+
const localGlobalEnvPath = "config/.env.global";
|
88
|
+
const localEnvPath = `config/.env.${cmdObj.env}`;
|
89
|
+
const destEnvPath = path.join(destFolder, `config/.env.${cmdObj.env}`);
|
90
|
+
if (fs.existsSync(localGlobalEnvPath)) {
|
91
|
+
console.warn(`COPYING ${localGlobalEnvPath} to ${destGlobalEnvPath}`);
|
92
|
+
fs.copyFileSync(localGlobalEnvPath, destGlobalEnvPath);
|
93
|
+
anyCopied = true;
|
94
|
+
}
|
95
|
+
if (fs.existsSync(localEnvPath)) {
|
96
|
+
console.warn(`COPYING ${localEnvPath} to ${destEnvPath}`);
|
97
|
+
fs.copyFileSync(localEnvPath, destEnvPath);
|
98
|
+
anyCopied = true;
|
99
|
+
}
|
100
|
+
if (!anyCopied) {
|
101
|
+
console.warn(chalk.red(`\nWarning: local environment ${cmdObj.env} has no .env files. Environment variables will not be injected.\n`));
|
102
|
+
}
|
103
|
+
}
|
76
104
|
|
77
105
|
// Copy all dependencies
|
78
106
|
getImageDescendentData(image).forEach((project) => {
|
package/src/cli/redis.ts
CHANGED
@@ -64,7 +64,7 @@ function run(cmdObj: CLICommandParser) {
|
|
64
64
|
const parsed = cmdObj.parseOptions({ params: ["-p"] });
|
65
65
|
|
66
66
|
const [command, namespace] = parsed.args;
|
67
|
-
const port = parsed.options["-p"]
|
67
|
+
const port = parsed.options["-p"];
|
68
68
|
// @ts-expect-error left as an exercise for the reader
|
69
69
|
const handler = handlers[command];
|
70
70
|
if (!handler) {
|
package/src/cli/registry.ts
CHANGED
@@ -11,21 +11,27 @@ const oneLiner = "Manage container repositories";
|
|
11
11
|
const keyExamples = `
|
12
12
|
$ devops registry server-url
|
13
13
|
$ devops registry reg-url
|
14
|
-
$ devops registry repo-url
|
15
|
-
$ devops registry
|
14
|
+
$ devops registry repo-url my-image sha
|
15
|
+
$ devops registry image-name my-image
|
16
|
+
$ devops registry prune my-image
|
16
17
|
`.trim();
|
17
18
|
|
18
19
|
const usage = `
|
19
20
|
${oneLiner}
|
20
21
|
|
21
22
|
USAGE
|
22
|
-
Get base URLs
|
23
|
+
Get base URLs for the container registry of the cluster:
|
23
24
|
devops registry server-url
|
24
25
|
devops registry reg-url
|
25
26
|
|
27
|
+
Note: for cloudrun images these URLs are not relevant.
|
28
|
+
|
26
29
|
Gets the URL of an image in the container registry:
|
27
30
|
devops registry repo-url <image> <sha> --env <env>
|
28
31
|
|
32
|
+
Gets the image name in the container registry:
|
33
|
+
devops registry image-name <image> --env <env>
|
34
|
+
|
29
35
|
Prunes the repository of old images to enforce the "image-versions-to-keep" constant in config/constants.yaml:
|
30
36
|
devops registry prune <image> --env <env>
|
31
37
|
|
@@ -47,13 +53,17 @@ const handlers = {
|
|
47
53
|
)
|
48
54
|
);
|
49
55
|
},
|
56
|
+
"image-name": (opts: StrongParams) => {
|
57
|
+
console.log(containerRegistryImageName(opts.required("image"), opts.required("env")));
|
58
|
+
},
|
50
59
|
prune: (opts: StrongParams) => {
|
51
60
|
const regName = containerRegistryPath();
|
61
|
+
const image = opts.required("image");
|
52
62
|
const repoName = containerRegistryImageName(
|
53
|
-
|
63
|
+
image,
|
54
64
|
opts.required("env")
|
55
65
|
);
|
56
|
-
prune(regName, repoName);
|
66
|
+
prune(regName, repoName, image);
|
57
67
|
},
|
58
68
|
};
|
59
69
|
|
package/src/devops.ts
CHANGED
@@ -22,6 +22,7 @@ import namespace from "./cli/namespace";
|
|
22
22
|
import image from "./cli/image";
|
23
23
|
import template from "./cli/template";
|
24
24
|
import job from "./cli/job";
|
25
|
+
import cloudrun from "./cli/cloudrun";
|
25
26
|
|
26
27
|
const [_node, _scriptPath, ...commandArgs] = process.argv;
|
27
28
|
|
@@ -45,11 +46,12 @@ const allImports = [
|
|
45
46
|
job,
|
46
47
|
//= Deployment
|
47
48
|
prepBuild,
|
49
|
+
cloudrun,
|
48
50
|
affected,
|
49
51
|
constant,
|
50
52
|
registry,
|
51
53
|
internalCurl,
|
52
|
-
jwt
|
54
|
+
jwt,
|
53
55
|
];
|
54
56
|
|
55
57
|
const commands: {
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import { randomBytes } from "crypto";
|
2
|
+
import { CommandExecutor } from "../cli/common";
|
3
|
+
import { containerRegistryRepoPath, isLocalOrRemoteEnv } from "./k8s-constants";
|
4
|
+
import { getImageData } from "./config";
|
5
|
+
import { getMonorepoSecretObject } from "./k8s-secrets-manager";
|
6
|
+
import chalk from "chalk";
|
7
|
+
|
8
|
+
function verifyCloudrunImage(image: string) {
|
9
|
+
const imageData = getImageData(image);
|
10
|
+
if (!imageData["cloudrun"]) {
|
11
|
+
console.error(`Image ${image} is not a cloudrun image. Add "cloudrun: true" in images.yaml`);
|
12
|
+
process.exit(1);
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
function getEnvValuesToForward(env: string, forwardEnv: string[]) {
|
17
|
+
if (!forwardEnv.length) return {};
|
18
|
+
|
19
|
+
let envValues: Record<string, string> = {};
|
20
|
+
const missingValues = new Set<string>();
|
21
|
+
for (const key of forwardEnv) {
|
22
|
+
const value = process.env[key];
|
23
|
+
if (value) {
|
24
|
+
envValues[key] = value;
|
25
|
+
} else {
|
26
|
+
missingValues.add(key);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
if (missingValues.size > 0 && isLocalOrRemoteEnv(env) === "remote") {
|
30
|
+
const secretsFromCluster = getMonorepoSecretObject(env, Array.from(missingValues));
|
31
|
+
for (const key of Object.keys(secretsFromCluster)) {
|
32
|
+
envValues[key] = secretsFromCluster[key];
|
33
|
+
missingValues.delete(key);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
if (missingValues.size > 0) {
|
37
|
+
console.error(`Some forwardEnv variables are missing: ${Array.from(missingValues).join(", ")}`);
|
38
|
+
process.exit(1);
|
39
|
+
}
|
40
|
+
return envValues;
|
41
|
+
}
|
42
|
+
|
43
|
+
export async function buildDev(image: string) {
|
44
|
+
verifyCloudrunImage(image);
|
45
|
+
const env = "development";
|
46
|
+
const sha = randomBytes(12).toString("hex");
|
47
|
+
|
48
|
+
const buildDir = new CommandExecutor(`devops prep-build ${image}`, {
|
49
|
+
env,
|
50
|
+
}).exec().trim();
|
51
|
+
|
52
|
+
const tag = containerRegistryRepoPath(image, env, sha);
|
53
|
+
console.warn(`Building ${tag} from ${buildDir}`);
|
54
|
+
|
55
|
+
await new CommandExecutor(
|
56
|
+
`docker build --platform linux/amd64 -t ${tag} ${buildDir} --build-arg MONOREPO_ENV=${env}`,
|
57
|
+
{ env }
|
58
|
+
).spawn();
|
59
|
+
|
60
|
+
console.warn(`Pushing ${tag}`);
|
61
|
+
await new CommandExecutor(`docker push ${tag}`, { env }).spawn();
|
62
|
+
|
63
|
+
console.warn(`\n✅ Built and pushed ${tag}\n`);
|
64
|
+
console.warn('Run "devops cloudrun deploy" next. For example:')
|
65
|
+
console.warn(chalk.blue(`./devops cloudrun deploy ${image} ${sha} --env ${env} --allow-unauthenticated --region us-east1 --forward-env ENV1,ENV2`));
|
66
|
+
console.warn();
|
67
|
+
console.log(tag);
|
68
|
+
}
|
69
|
+
|
70
|
+
export async function deploy({
|
71
|
+
image,
|
72
|
+
env,
|
73
|
+
sha,
|
74
|
+
region,
|
75
|
+
forwardEnv = [],
|
76
|
+
allowUnauthenticated = false,
|
77
|
+
cpu = "0.25",
|
78
|
+
memory = "256Mi",
|
79
|
+
minInstances = 0,
|
80
|
+
maxInstances = 1,
|
81
|
+
timeout = "60s",
|
82
|
+
extraArgs = "",
|
83
|
+
}: {
|
84
|
+
image: string;
|
85
|
+
env: string;
|
86
|
+
sha: string;
|
87
|
+
region: string;
|
88
|
+
forwardEnv?: string[];
|
89
|
+
allowUnauthenticated?: boolean;
|
90
|
+
cpu?: string;
|
91
|
+
memory?: string;
|
92
|
+
minInstances?: number;
|
93
|
+
maxInstances?: number;
|
94
|
+
timeout?: string;
|
95
|
+
extraArgs?: string;
|
96
|
+
}) {
|
97
|
+
verifyCloudrunImage(image);
|
98
|
+
const repoPath = containerRegistryRepoPath(image, env, sha);
|
99
|
+
const envValues = getEnvValuesToForward(env, forwardEnv);
|
100
|
+
const envValuesCsv = Object.entries(envValues).map(([key, value]) => `${key}="${value}"`).join(",");
|
101
|
+
const serviceName = `${image}-${env}`;
|
102
|
+
|
103
|
+
const cmd = `
|
104
|
+
gcloud run deploy ${serviceName}
|
105
|
+
--image ${repoPath}
|
106
|
+
${Object.keys(envValues).length > 0 ? `--set-env-vars ${envValuesCsv}` : ""}
|
107
|
+
${allowUnauthenticated ? "--allow-unauthenticated" : ""}
|
108
|
+
--region ${region}
|
109
|
+
--cpu ${cpu}
|
110
|
+
--memory ${memory}
|
111
|
+
--min-instances ${minInstances}
|
112
|
+
--max-instances ${maxInstances}
|
113
|
+
--timeout ${timeout}
|
114
|
+
${extraArgs}
|
115
|
+
`.trim().replace(/\s+/g, " ");
|
116
|
+
|
117
|
+
await new CommandExecutor(cmd, { env }).spawn();
|
118
|
+
}
|
package/src/libs/config.ts
CHANGED
@@ -11,6 +11,11 @@ const imagesFilePath = path.join(process.cwd(), ".devops/config/images.yaml");
|
|
11
11
|
export const { getConst } = processConstFile();
|
12
12
|
export const { getImageData, getImageNames, getTemplateData } = processImagesFile();
|
13
13
|
|
14
|
+
export function getImageType(image: string) {
|
15
|
+
const imageData = getImageData(image);
|
16
|
+
return imageData["cloudrun"] ? "cloudrun" : "k8s";
|
17
|
+
}
|
18
|
+
|
14
19
|
// Process config/constants.yaml
|
15
20
|
|
16
21
|
function processConstFile() {
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { CommandExecutor } from "../../cli/common";
|
2
2
|
import { z } from "zod";
|
3
|
-
import { getConst } from "../config";
|
3
|
+
import { getConst, getImageData } from "../config";
|
4
4
|
|
5
5
|
const repoTagMetadataSchema = z.object({
|
6
6
|
// What we rely on
|
@@ -60,7 +60,8 @@ export function prune(
|
|
60
60
|
/** To keep the image-related constants simple, this accepts the full URL including the prefix registry.digitalocean.com */
|
61
61
|
registryFullName: string,
|
62
62
|
/** The name of the repository inside the registry */
|
63
|
-
repoName: string
|
63
|
+
repoName: string,
|
64
|
+
image: string
|
64
65
|
) {
|
65
66
|
const infra = getConst("infra");
|
66
67
|
if (infra !== "digitalocean") {
|
@@ -69,6 +70,13 @@ export function prune(
|
|
69
70
|
);
|
70
71
|
return;
|
71
72
|
}
|
73
|
+
const imageData = getImageData(image);
|
74
|
+
if (imageData["cloudrun"]) {
|
75
|
+
console.warn(
|
76
|
+
"Pruning is skipped for cloudrun images"
|
77
|
+
);
|
78
|
+
return;
|
79
|
+
}
|
72
80
|
const tags = getRepoTagMetadata(repoName);
|
73
81
|
const versionsToKeep = Number(getConst("image-versions-to-keep"));
|
74
82
|
if (!tags.length || tags.length <= versionsToKeep) return;
|
@@ -24,13 +24,28 @@ export function allSupportedEnvs() {
|
|
24
24
|
return [...remoteSupportedEnvs(), ...localSupportedEnvs()];
|
25
25
|
}
|
26
26
|
|
27
|
-
function
|
27
|
+
export function isLocalOrRemoteEnv(monorepoEnv: string): "local" | "remote" {
|
28
|
+
if (remoteSupportedEnvs().includes(monorepoEnv)) return "remote";
|
29
|
+
if (localSupportedEnvs().includes(monorepoEnv)) return "local";
|
30
|
+
throw new Error(`Unsupported environment: ${monorepoEnv}`);
|
31
|
+
}
|
32
|
+
|
33
|
+
function validateEnv(monorepoEnv?: string, { allowLocal = false }: { allowLocal?: boolean } = {}) {
|
28
34
|
if (!monorepoEnv) throw new Error("MONOREPO_ENV cannot be empty");
|
29
|
-
if (
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
35
|
+
if (allowLocal) {
|
36
|
+
if (!allSupportedEnvs().includes(monorepoEnv)) {
|
37
|
+
console.error(
|
38
|
+
`MONOREPO_ENV must be one of: ${allSupportedEnvs().join(", ")}. Can be set using --env flag.`
|
39
|
+
);
|
40
|
+
process.exit(1);
|
41
|
+
}
|
42
|
+
} else {
|
43
|
+
if (!remoteSupportedEnvs().includes(monorepoEnv)) {
|
44
|
+
console.error(
|
45
|
+
`MONOREPO_ENV must be one of: ${remoteSupportedEnvs().join(", ")}. Can be set using --env flag.`
|
46
|
+
);
|
47
|
+
process.exit(1);
|
48
|
+
}
|
34
49
|
}
|
35
50
|
}
|
36
51
|
|
@@ -52,7 +67,7 @@ export function imageConfigMap(image: string) {
|
|
52
67
|
}
|
53
68
|
|
54
69
|
export function containerRegistryImageName(image: string, monorepoEnv: string) {
|
55
|
-
validateEnv(monorepoEnv);
|
70
|
+
validateEnv(monorepoEnv, { allowLocal: true });
|
56
71
|
return `${getConst("project-name")}-${monorepoEnv}-${image}`;
|
57
72
|
}
|
58
73
|
|
@@ -65,11 +80,20 @@ export function containerRegistryRepoPath(
|
|
65
80
|
monorepoEnv: string,
|
66
81
|
gitSha: string
|
67
82
|
) {
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
[
|
72
|
-
|
83
|
+
const imageNameAndTag = [containerRegistryImageName(image, monorepoEnv), gitSha].join(":");
|
84
|
+
const imageData = getImageData(image);
|
85
|
+
if (imageData["cloudrun"]) {
|
86
|
+
return [
|
87
|
+
getConst("cloudrun-artifact-registry-repo-path"),
|
88
|
+
imageNameAndTag,
|
89
|
+
].join("/");
|
90
|
+
} else {
|
91
|
+
return [
|
92
|
+
getConst("registry-base-url"),
|
93
|
+
getConst("registry-image-path-prefix", { ignoreIfInvalid: true }),
|
94
|
+
imageNameAndTag,
|
95
|
+
].filter(Boolean).join("/");
|
96
|
+
}
|
73
97
|
}
|
74
98
|
|
75
99
|
export function domainNameForEnv(image: string, monorepoEnv: string) {
|