@catladder/pipeline 4.7.0 → 4.7.1

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.
@@ -5,6 +5,11 @@ import {
5
5
  } from "../../../bash";
6
6
  import type { ComponentContext } from "../../../types";
7
7
  import { allowFailureInScripts, repeatOnFailure } from "../../../utils/gitlab";
8
+ import type { VariableValue } from "../../../variables/VariableValue";
9
+ import {
10
+ VariableReference,
11
+ VariableValueContainingReferences,
12
+ } from "../../../variables/VariableValueContainingReferences";
8
13
  import type {
9
14
  DeployConfigCloudRun,
10
15
  DeployConfigCloudRunCloudSql,
@@ -73,7 +78,7 @@ export type DBVariables = {
73
78
  * controls how variables in the connection string are handled
74
79
  *
75
80
  * - legacy: variables like $DB_USER will be kept as environment variables to be replaced at runtime (default). It is not using BashExpressions as this was not the case in the past.
76
- * - embedded: variables will be replaced with their actual values in the connection string
81
+ * - embedded: variables will be replaced with the component's final values in the connection string. This makes the connection string usable from other components (e.g. via ${otherComponent:DATABASE_URL}) and respects overrides of DB_USER, DB_PASSWORD, etc. in vars.public (e.g. DB_PASSWORD: "${otherComponent:DB_PASSWORD}" when reusing another component's database)
77
82
  *
78
83
  * We will remove the legacy mode in the future, as it is confusing. But its unclear whether its a breaking change in some edge cases
79
84
  */
@@ -81,82 +86,109 @@ export type DBVariablesMode = "legacy" | "embedded";
81
86
 
82
87
  export const DEFAULT_DB_VARIABLES_MODE: DBVariablesMode = "legacy";
83
88
 
89
+ type DbUrlPart = StringOrBashExpression | VariableReference;
90
+
84
91
  const getVariableOrValue = (
85
92
  key: keyof DBVariables,
86
- variables: DBVariables,
87
93
  mode: DBVariablesMode,
88
- ): StringOrBashExpression => {
89
- return mode === "legacy" ? `$${key}` : variables[key];
94
+ componentName: string,
95
+ ): DbUrlPart => {
96
+ // in embedded mode we reference the component's own env var, which gets
97
+ // resolved to its final value (including vars.public overrides) after all
98
+ // env vars have been merged
99
+ return mode === "legacy"
100
+ ? `$${key}`
101
+ : new VariableReference(componentName, key);
90
102
  };
91
103
 
104
+ const joinDbUrlParts = (
105
+ parts: DbUrlPart[],
106
+ mode: DBVariablesMode,
107
+ ): VariableValue =>
108
+ mode === "legacy"
109
+ ? joinBashExpressions(parts as StringOrBashExpression[])
110
+ : new VariableValueContainingReferences(parts);
111
+
92
112
  export const getDatabaseJdbcUrl = (
93
113
  variables: DBVariables,
94
114
  mode: DBVariablesMode,
115
+ componentName: string,
95
116
  ) => {
96
117
  const parts = [
97
118
  "jdbc:postgresql:///",
98
- getVariableOrValue("DB_NAME", variables, mode),
119
+ getVariableOrValue("DB_NAME", mode, componentName),
99
120
  "?cloudSqlInstance=",
100
- getVariableOrValue("CLOUD_SQL_INSTANCE_CONNECTION_NAME", variables, mode),
121
+ getVariableOrValue(
122
+ "CLOUD_SQL_INSTANCE_CONNECTION_NAME",
123
+ mode,
124
+ componentName,
125
+ ),
101
126
  "&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=",
102
- getVariableOrValue("DB_USER", variables, mode),
127
+ getVariableOrValue("DB_USER", mode, componentName),
103
128
  "&password=",
104
- getVariableOrValue("DB_PASSWORD", variables, mode),
129
+ getVariableOrValue("DB_PASSWORD", mode, componentName),
105
130
  ];
106
131
 
107
- return joinBashExpressions(parts);
132
+ return joinDbUrlParts(parts, mode);
108
133
  };
109
134
 
110
135
  export const getRailsDatabaseConnectionString = (
111
136
  variables: DBVariables,
112
137
  mode: DBVariablesMode,
138
+ componentName: string,
113
139
  ) => {
114
140
  const parts = [
115
141
  "postgresql://",
116
- getVariableOrValue("DB_USER", variables, mode),
142
+ getVariableOrValue("DB_USER", mode, componentName),
117
143
  ":",
118
- getVariableOrValue("DB_PASSWORD", variables, mode),
144
+ getVariableOrValue("DB_PASSWORD", mode, componentName),
119
145
  "@",
120
146
  encodeURIComponent(
121
147
  `/cloudsql/${variables.CLOUD_SQL_INSTANCE_CONNECTION_NAME}`,
122
148
  ),
123
149
  "/",
124
- getVariableOrValue("DB_NAME", variables, mode),
150
+ getVariableOrValue("DB_NAME", mode, componentName),
125
151
  "?",
126
152
  ];
127
- return joinBashExpressions(parts);
153
+ return joinDbUrlParts(parts, mode);
128
154
  };
129
155
 
130
156
  export const getPrismaDatabaseConnectionString = (
131
157
  variables: DBVariables,
132
158
  mode: DBVariablesMode,
159
+ componentName: string,
133
160
  ) => {
134
161
  const parts = [
135
162
  "postgresql://",
136
- getVariableOrValue("DB_USER", variables, mode),
163
+ getVariableOrValue("DB_USER", mode, componentName),
137
164
  ":",
138
- getVariableOrValue("DB_PASSWORD", variables, mode),
165
+ getVariableOrValue("DB_PASSWORD", mode, componentName),
139
166
  "@localhost/",
140
- getVariableOrValue("DB_NAME", variables, mode),
167
+ getVariableOrValue("DB_NAME", mode, componentName),
141
168
  "?host=/cloudsql/",
142
- getVariableOrValue("CLOUD_SQL_INSTANCE_CONNECTION_NAME", variables, mode),
169
+ getVariableOrValue(
170
+ "CLOUD_SQL_INSTANCE_CONNECTION_NAME",
171
+ mode,
172
+ componentName,
173
+ ),
143
174
  ];
144
- return joinBashExpressions(parts);
175
+ return joinDbUrlParts(parts, mode);
145
176
  };
146
177
 
147
178
  export const getDatabaseConnectionString = (
148
179
  config: DeployConfigCloudRunCloudSql,
149
180
  variables: DBVariables,
150
- ): StringOrBashExpression => {
181
+ componentName: string,
182
+ ): VariableValue => {
151
183
  const mode =
152
184
  config.dbConnectionStringVariablesMode ?? DEFAULT_DB_VARIABLES_MODE;
153
185
  switch (config.dbConnectionStringFormat) {
154
186
  case "jdbc":
155
- return getDatabaseJdbcUrl(variables, mode);
187
+ return getDatabaseJdbcUrl(variables, mode, componentName);
156
188
  case "rails":
157
- return getRailsDatabaseConnectionString(variables, mode);
189
+ return getRailsDatabaseConnectionString(variables, mode, componentName);
158
190
  default:
159
191
  // prisma
160
- return getPrismaDatabaseConnectionString(variables, mode);
192
+ return getPrismaDatabaseConnectionString(variables, mode, componentName);
161
193
  }
162
194
  };
@@ -1,9 +1,9 @@
1
1
  import type { BuildConfig, SecretEnvVar } from "..";
2
- import type { BashExpression } from "../bash/BashExpression";
3
2
  import type { ComponentContext } from "../types/context";
4
3
  import type { EnvironmentContext } from "../types/environmentContext";
5
4
  import type { CatladderJob } from "../types/jobs";
6
5
  import type { PartialDeep } from "../types/utils";
6
+ import type { VariableValue } from "../variables/VariableValue";
7
7
  import { GCLOUD_RUN_DEPLOY_TYPE } from "./cloudRun";
8
8
  import { CUSTOM_DEPLOY_TYPE } from "./custom";
9
9
  import { DOCKER_TAG_DEPLOY_TYPE } from "./dockerTag";
@@ -26,7 +26,7 @@ export type DeployTypeDefinition<D extends DeployConfig> = {
26
26
  ) => SecretEnvVar[];
27
27
  getAdditionalEnvVars: (
28
28
  envContext: EnvironmentContext<BuildConfig, D>,
29
- ) => Record<string, string | BashExpression | undefined | null>;
29
+ ) => Record<string, VariableValue | undefined | null>;
30
30
  /**
31
31
  * script lines the deploy type contributes to the start of the verify job,
32
32
  * e.g. to authenticate against a non-public service
@@ -43,6 +43,17 @@ export class VariableValueContainingReferences {
43
43
  );
44
44
  }
45
45
 
46
+ /**
47
+ * concats values to this one and returns a new VariableValueContainingReferences.
48
+ * mirrors String.prototype.concat / BashExpression.concat so a VariableValue
49
+ * can be extended regardless of its concrete type
50
+ */
51
+ public concat(
52
+ ...values: Array<VariableValuePart | VariableValueContainingReferences>
53
+ ): VariableValueContainingReferences {
54
+ return new VariableValueContainingReferences([...this.parts, ...values]);
55
+ }
56
+
46
57
  public toString(
47
58
  options: EscapeOptions = {
48
59
  quotes: false,
@@ -1,5 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
+ import { getBashVariable } from "../../bash/BashExpression";
2
3
  import {
4
+ VariableReference,
3
5
  VariableValueContainingReferences,
4
6
  createVariableValueContainingReferencesFromString,
5
7
  } from "../VariableValueContainingReferences";
@@ -68,6 +70,53 @@ describe("replaceAllReferences", () => {
68
70
  });
69
71
  });
70
72
 
73
+ it("passes through plain strings and bash expressions and resolves self references against the map fetched for the own component", async () => {
74
+ const DB_PASSWORD_SECRET = getBashVariable("CL_dev_api_DB_PASSWORD");
75
+
76
+ // a merged env var map as produced by getEnvironmentVariables: mostly
77
+ // plain values, plus a framework-generated value containing self
78
+ // references (like the embedded database connection string)
79
+ const values = {
80
+ DB_USER: "my-user",
81
+ DB_PASSWORD: DB_PASSWORD_SECRET,
82
+ DATABASE_URL: new VariableValueContainingReferences([
83
+ "postgresql://",
84
+ new VariableReference("worker", "DB_USER"),
85
+ ":",
86
+ new VariableReference("worker", "DB_PASSWORD"),
87
+ "@localhost/db",
88
+ ]),
89
+ EMPTY: null,
90
+ };
91
+
92
+ const getEnvVars = async (componentName: string) => {
93
+ expect(componentName).toBe("worker");
94
+ return {
95
+ DB_USER: "my-user",
96
+ // the fetched map contains the override, like a vars.public override
97
+ // referencing another component's password
98
+ DB_PASSWORD: DB_PASSWORD_SECRET,
99
+ };
100
+ };
101
+
102
+ const result = await resolveAllReferences(values, getEnvVars);
103
+ expect(result.DB_USER).toBe("my-user");
104
+ expect(result.DB_PASSWORD).toBe(DB_PASSWORD_SECRET);
105
+ expect(result.EMPTY).toBe(null);
106
+ expect(result.DATABASE_URL).toEqual(
107
+ new VariableValueContainingReferences([
108
+ "postgresql://",
109
+ "my-user",
110
+ ":",
111
+ DB_PASSWORD_SECRET,
112
+ "@localhost/db",
113
+ ]),
114
+ );
115
+ expect(result.DATABASE_URL.toString()).toBe(
116
+ "postgresql://my-user:$CL_dev_api_DB_PASSWORD@localhost/db",
117
+ );
118
+ });
119
+
71
120
  it("detects infinte loop", async () => {
72
121
  const values = {
73
122
  myVar: createVariableValueContainingReferencesFromString(
@@ -1,31 +1,35 @@
1
1
  import type { VariableValue } from "./VariableValue";
2
- import type { VariableValueContainingReferences } from "./VariableValueContainingReferences";
3
- import { VariableReference } from "./VariableValueContainingReferences";
2
+ import {
3
+ VariableReference,
4
+ VariableValueContainingReferences,
5
+ } from "./VariableValueContainingReferences";
4
6
  import { resolveAllReferencesOnce as resolveAllReferencesOnce } from "./resolveAllReferencesOnce";
5
7
 
6
- export const resolveAllReferences = async (
7
- values: Record<string, VariableValueContainingReferences>,
8
+ const hasUnresolvedReferences = (value: VariableValue | null | undefined) =>
9
+ value instanceof VariableValueContainingReferences &&
10
+ value.parts.some((part) => part instanceof VariableReference);
11
+
12
+ export const resolveAllReferences = async <
13
+ T extends Record<string, VariableValue | null | undefined>,
14
+ >(
15
+ values: T,
8
16
  getEnvVars: (
9
17
  componentName: string,
10
18
  ) => Promise<Record<string, VariableValue | null | undefined>>,
11
- ) => {
19
+ ): Promise<T> => {
12
20
  // replace until there aren't any references left
13
21
  let result = values;
14
22
 
15
23
  let i = 0;
16
24
 
17
- while (
18
- Object.values(result).some((value) =>
19
- value.parts.some((part) => part instanceof VariableReference),
20
- )
21
- ) {
25
+ while (Object.values(result).some(hasUnresolvedReferences)) {
22
26
  const replaced = await resolveAllReferencesOnce(result, getEnvVars);
23
27
 
24
28
  result = replaced;
25
29
  i++;
26
30
  if (i > 1000) {
27
31
  const unresolved = Object.entries(result).filter(([key, value]) =>
28
- value.parts.some((part) => part instanceof VariableReference),
32
+ hasUnresolvedReferences(value),
29
33
  );
30
34
 
31
35
  throw new Error(
@@ -33,9 +37,9 @@ export const resolveAllReferences = async (
33
37
  unresolved
34
38
  .map(
35
39
  ([key, value]) =>
36
- `${key} (last reference: ${value.parts.find(
37
- (part) => part instanceof VariableReference,
38
- )})`,
40
+ `${key} (last reference: ${(
41
+ value as VariableValueContainingReferences
42
+ ).parts.find((part) => part instanceof VariableReference)})`,
39
43
  )
40
44
  .join(", "),
41
45
  );
@@ -1,19 +1,24 @@
1
1
  import type { VariableValue } from "./VariableValue";
2
- import type { VariableValueContainingReferences } from "./VariableValueContainingReferences";
3
- import { VariableReference } from "./VariableValueContainingReferences";
2
+ import {
3
+ VariableReference,
4
+ VariableValueContainingReferences,
5
+ } from "./VariableValueContainingReferences";
4
6
  import { resolveReferencesOnce } from "./resolveReferencesOnce";
5
7
 
6
- export const resolveAllReferencesOnce = async (
7
- values: Record<string, VariableValueContainingReferences>,
8
+ export const resolveAllReferencesOnce = async <
9
+ T extends Record<string, VariableValue | null | undefined>,
10
+ >(
11
+ values: T,
8
12
  getEnvVars: (
9
13
  componentName: string,
10
14
  ) => Promise<Record<string, VariableValue | undefined | null>>,
11
- ) => {
12
- const allReferences = Object.values(values).flatMap(
13
- (value) =>
14
- value?.parts.filter(
15
- (part) => part instanceof VariableReference,
16
- ) as VariableReference[],
15
+ ): Promise<T> => {
16
+ const allReferences = Object.values(values).flatMap((value) =>
17
+ value instanceof VariableValueContainingReferences
18
+ ? (value.parts.filter(
19
+ (part) => part instanceof VariableReference,
20
+ ) as VariableReference[])
21
+ : [],
17
22
  );
18
23
 
19
24
  const allComponentsUnique = Array.from(
@@ -34,11 +39,11 @@ export const resolveAllReferencesOnce = async (
34
39
  return Object.fromEntries(
35
40
  Object.entries(values).map(([key, value]) => [
36
41
  key,
37
- value !== null && value !== undefined
42
+ value instanceof VariableValueContainingReferences
38
43
  ? resolveReferencesOnce(value, ({ componentName, variableName }) => {
39
44
  return allEnvVarsInComponents[componentName][variableName];
40
45
  })
41
46
  : value,
42
47
  ]),
43
- );
48
+ ) as T;
44
49
  };