@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.
Files changed (59) hide show
  1. package/README.md +5 -1
  2. package/dist/cli/cloudrun.d.ts +11 -0
  3. package/dist/cli/cloudrun.d.ts.map +1 -0
  4. package/dist/cli/cloudrun.js +121 -0
  5. package/dist/cli/common.d.ts +10 -12
  6. package/dist/cli/common.d.ts.map +1 -1
  7. package/dist/cli/common.js +28 -22
  8. package/dist/cli/env.js +2 -2
  9. package/dist/cli/image.d.ts.map +1 -1
  10. package/dist/cli/image.js +16 -1
  11. package/dist/cli/prep-build.d.ts.map +1 -1
  12. package/dist/cli/prep-build.js +35 -6
  13. package/dist/cli/registry.d.ts.map +1 -1
  14. package/dist/cli/registry.js +15 -5
  15. package/dist/devops.js +3 -1
  16. package/dist/libs/cloudrun-helpers.d.ts +16 -0
  17. package/dist/libs/cloudrun-helpers.d.ts.map +1 -0
  18. package/dist/libs/cloudrun-helpers.js +79 -0
  19. package/dist/libs/config.d.ts +1 -0
  20. package/dist/libs/config.d.ts.map +1 -1
  21. package/dist/libs/config.js +4 -0
  22. package/dist/libs/digital-ocean/container-reg.d.ts +1 -1
  23. package/dist/libs/digital-ocean/container-reg.d.ts.map +1 -1
  24. package/dist/libs/digital-ocean/container-reg.js +7 -2
  25. package/dist/libs/k8s-constants.d.ts +1 -0
  26. package/dist/libs/k8s-constants.d.ts.map +1 -1
  27. package/dist/libs/k8s-constants.js +35 -10
  28. package/dist/libs/k8s-generate.d.ts +1 -1
  29. package/dist/libs/k8s-generate.d.ts.map +1 -1
  30. package/dist/libs/k8s-generate.js +15 -2
  31. package/dist/libs/k8s-secrets-manager.d.ts +2 -1
  32. package/dist/libs/k8s-secrets-manager.d.ts.map +1 -1
  33. package/dist/libs/k8s-secrets-manager.js +8 -5
  34. package/dist/types/index.d.ts +19 -8
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js +3 -1
  37. package/package.json +1 -1
  38. package/src/cli/cloudrun.ts +133 -0
  39. package/src/cli/common.ts +46 -38
  40. package/src/cli/db.ts +1 -1
  41. package/src/cli/dml.ts +1 -1
  42. package/src/cli/env.ts +2 -2
  43. package/src/cli/exec.ts +1 -1
  44. package/src/cli/image.ts +15 -1
  45. package/src/cli/job.ts +1 -1
  46. package/src/cli/prep-build.ts +34 -6
  47. package/src/cli/redis.ts +1 -1
  48. package/src/cli/registry.ts +15 -5
  49. package/src/devops.ts +3 -1
  50. package/src/libs/cloudrun-helpers.ts +118 -0
  51. package/src/libs/config.ts +5 -0
  52. package/src/libs/digital-ocean/container-reg.ts +10 -2
  53. package/src/libs/k8s-constants.ts +36 -12
  54. package/src/libs/k8s-generate.ts +15 -2
  55. package/src/libs/k8s-secrets-manager.ts +9 -5
  56. package/src/target-templates/lang-variants-common/python/.devops/config/images.yaml +3 -1
  57. package/src/target-templates/lang-variants-common/typescript/.devops/docker-images/cloudrun.Dockerfile +31 -0
  58. package/src/target-templates/lang-variants-common/typescript/.github/actions/deploy-image@v1/action.yaml +4 -0
  59. 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: { [key: string]: string | boolean };
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?: string[];
76
+ params?: TParamKeys
77
77
  /** Boolean flag is used like so: --flag */
78
- booleans?: string[];
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?: string[];
108
- booleans?: string[];
107
+ params?: TParamKeys;
108
+ booleans?: TBoolKeys;
109
109
  passthroughArgs?: boolean;
110
110
  } = {}
111
- ): ParsedArgs {
112
- const results: ParsedArgs = {
113
- args: [],
114
- argsStr: "",
115
- options: {},
116
- ...(passthroughArgs ? { passthrough: [] } : {}),
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
- const numArgsToProcess =
122
- passthroughArgsStart === -1 ? args.length : passthroughArgsStart;
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 (paramsLookup[curr]) {
132
+ if (isParam(curr)) {
126
133
  const next = args[i + 1];
127
- results.options[curr] = next;
134
+ resParams[curr] = next;
128
135
  ++i;
129
- } else if (booleansLookup[curr]) {
130
- results.options[curr] = true;
136
+ } else if (isBoolean(curr)) {
137
+ resOptions[curr] = true;
131
138
  } else {
132
- results.args.push(curr);
139
+ resArgs.push(curr);
133
140
  }
134
141
  }
135
- results.argsStr = results.args.join(" ");
136
- if (passthroughArgs && passthroughArgsStart >= 0) {
137
- results.passthrough = args.slice(passthroughArgsStart + 1);
138
- }
139
- return results;
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 [cmd, ...args] = fullCommand.split(" ").filter(Boolean);
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"] as string;
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"] as string;
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
- getMonorepoSecret,
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(getMonorepoSecret(cmdObj.env, rest));
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"] as string | undefined;
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 === 'scale') {
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"] as string | undefined;
68
+ const timeout = parsedArgs.options["--timeout"];
69
69
  const params = new StrongParams(usage, {
70
70
  env: cmdObj.env,
71
71
  image,
@@ -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 { getMonorepoSecret } from "../libs/k8s-secrets-manager";
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
- console.warn(`COPYING Docker image payload`);
65
- fs.copySync(dockerImagePayloadPath, destFolder);
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 envFileData = getMonorepoSecret(cmdObj.env);
75
- fs.writeFileSync(path.join(destFolder, `config/.env.global`), envFileData);
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"] as string;
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) {
@@ -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 my-image sha
15
- $ devops registry prune my-image
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
- opts.required("image"),
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
+ }
@@ -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 validateEnv(monorepoEnv?: string) {
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 (!remoteSupportedEnvs().includes(monorepoEnv)) {
30
- console.error(
31
- `MONOREPO_ENV must be one of: ${remoteSupportedEnvs().join(", ")}. Can be set using --env flag.`
32
- );
33
- process.exit(1);
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
- return [
69
- getConst("registry-base-url"),
70
- getConst("registry-image-path-prefix", { ignoreIfInvalid: true }),
71
- [containerRegistryImageName(image, monorepoEnv), gitSha].join(":"),
72
- ].filter(Boolean).join("/");
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) {