create-svc 0.1.63 → 0.1.64
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 +1 -1
- package/src/service-runtime/cloudrun/bootstrap.ts +3 -4
- package/src/service-runtime/cloudrun/config.ts +3 -0
- package/src/service-runtime/cloudrun/lib.ts +30 -13
- package/src/service-runtime/cloudrun/temporal-config.test.ts +66 -0
- package/src/service-runtime/cloudrun/temporal-config.ts +84 -0
package/package.json
CHANGED
|
@@ -79,19 +79,18 @@ export async function prepareGcpProject() {
|
|
|
79
79
|
|
|
80
80
|
function publishTemporalSecrets() {
|
|
81
81
|
const temporal = resolveTemporalRuntimeConfig();
|
|
82
|
-
|
|
83
|
-
if (!apiKey || !temporal.apiKeySecretName) {
|
|
82
|
+
if (!temporal.apiKey || !temporal.apiKeySecretName) {
|
|
84
83
|
return "No Temporal API key configured";
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
addSecretVersion(temporal.apiKeySecretName, apiKey);
|
|
86
|
+
addSecretVersion(temporal.apiKeySecretName, temporal.apiKey);
|
|
88
87
|
ensureSecretAccessor(temporal.apiKeySecretName, `serviceAccount:${config.runtimeServiceAccount}`);
|
|
89
88
|
return temporal.apiKeySecretName;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
function shouldPublishTemporalSecrets() {
|
|
93
92
|
const temporal = resolveTemporalRuntimeConfig();
|
|
94
|
-
return Boolean(
|
|
93
|
+
return Boolean(temporal.enabled && temporal.apiKey && temporal.apiKeySecretName);
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
if (import.meta.main) {
|
|
@@ -3,6 +3,7 @@ import { serviceConfig } from "../runtime";
|
|
|
3
3
|
const cloudrun = serviceConfig.cloudrun;
|
|
4
4
|
const dns = serviceConfig.dns;
|
|
5
5
|
const neon = serviceConfig.neon;
|
|
6
|
+
const vault = serviceConfig.providers?.vault ?? {};
|
|
6
7
|
|
|
7
8
|
export const config = {
|
|
8
9
|
serviceName: serviceConfig.service_id,
|
|
@@ -39,6 +40,8 @@ export const config = {
|
|
|
39
40
|
namespace: serviceConfig.temporal.namespace,
|
|
40
41
|
taskQueue: serviceConfig.temporal.task_queue,
|
|
41
42
|
apiKeySecretName: serviceConfig.temporal.api_key_secret_name,
|
|
43
|
+
vaultMount: vault.mount || "secret",
|
|
44
|
+
vaultPath: vault.temporal_path || "prod/providers/temporal",
|
|
42
45
|
},
|
|
43
46
|
neon: {
|
|
44
47
|
projectId: neon.project_id,
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { config } from "./config";
|
|
4
4
|
import { serviceRoot } from "../runtime";
|
|
5
5
|
import { localDockerBuildArgs, parseDeployArgs, type DeployArgs } from "./deploy-args";
|
|
6
|
+
import { resolveTemporalRuntimeConfigValues } from "./temporal-config";
|
|
6
7
|
|
|
7
8
|
type CommandOptions = {
|
|
8
9
|
allowFailure?: boolean;
|
|
@@ -551,24 +552,40 @@ export async function renderManifest(image: string, target: DeploymentTarget, pr
|
|
|
551
552
|
}
|
|
552
553
|
|
|
553
554
|
export function resolveTemporalRuntimeConfig() {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const namespace = process.env.TEMPORAL_NAMESPACE?.trim() || config.temporal.namespace;
|
|
557
|
-
const taskQueue = process.env.TEMPORAL_TASK_QUEUE?.trim() || config.temporal.taskQueue;
|
|
558
|
-
const apiKeySecretName = process.env.TEMPORAL_API_KEY_SECRET?.trim() || (process.env.TEMPORAL_API_KEY?.trim() ? config.temporal.apiKeySecretName : "");
|
|
559
|
-
const enabled = enabledOverride
|
|
560
|
-
? ["1", "true", "yes", "on"].includes(enabledOverride.toLowerCase())
|
|
561
|
-
: config.temporal.enabled;
|
|
555
|
+
return resolveTemporalRuntimeConfigValues(config.temporal, process.env, readTemporalProviderFields);
|
|
556
|
+
}
|
|
562
557
|
|
|
558
|
+
function readTemporalProviderFields(mount: string, path: string) {
|
|
563
559
|
return {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
taskQueue,
|
|
568
|
-
apiKeySecretName,
|
|
560
|
+
address: readVaultField(mount, path, ["TEMPORAL_ADDRESS", "address"]),
|
|
561
|
+
namespace: readVaultField(mount, path, ["TEMPORAL_NAMESPACE", "namespace"]),
|
|
562
|
+
apiKey: readVaultField(mount, path, ["TEMPORAL_API_KEY", "api_key"]),
|
|
569
563
|
};
|
|
570
564
|
}
|
|
571
565
|
|
|
566
|
+
function readVaultField(mount: string, path: string, fields: string[]) {
|
|
567
|
+
const vault = Bun.which("vault");
|
|
568
|
+
if (!vault || !path) {
|
|
569
|
+
return "";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (const field of fields) {
|
|
573
|
+
const result = Bun.spawnSync([vault, "kv", "get", `-mount=${mount}`, `-field=${field}`, path], {
|
|
574
|
+
cwd: process.cwd(),
|
|
575
|
+
env: process.env,
|
|
576
|
+
stdout: "pipe",
|
|
577
|
+
stderr: "pipe",
|
|
578
|
+
});
|
|
579
|
+
if (result.success && result.stdout) {
|
|
580
|
+
const value = decoder.decode(result.stdout).trim();
|
|
581
|
+
if (value) {
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return "";
|
|
587
|
+
}
|
|
588
|
+
|
|
572
589
|
export async function writeRenderedManifest(image: string, target: DeploymentTarget) {
|
|
573
590
|
const rendered = await renderManifest(image, target);
|
|
574
591
|
const path = new URL("../../.cloudrun.rendered.yaml", import.meta.url);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { resolveTemporalRuntimeConfigValues } from "./temporal-config";
|
|
3
|
+
|
|
4
|
+
const baseConfig = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
address: "localhost:7233",
|
|
7
|
+
namespace: "default",
|
|
8
|
+
taskQueue: "orders",
|
|
9
|
+
apiKeySecretName: "orders-temporal-api-key",
|
|
10
|
+
vaultMount: "secret",
|
|
11
|
+
vaultPath: "prod/providers/temporal",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test("resolveTemporalRuntimeConfigValues reads production Temporal config from Vault fields", () => {
|
|
15
|
+
const resolved = resolveTemporalRuntimeConfigValues(baseConfig, {}, () => ({
|
|
16
|
+
address: "temporal.example.tmprl.cloud:7233",
|
|
17
|
+
namespace: "anmho.prod",
|
|
18
|
+
apiKey: "secret-key",
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
expect(resolved).toEqual({
|
|
22
|
+
enabled: true,
|
|
23
|
+
address: "temporal.example.tmprl.cloud:7233",
|
|
24
|
+
namespace: "anmho.prod",
|
|
25
|
+
taskQueue: "orders",
|
|
26
|
+
apiKeySecretName: "orders-temporal-api-key",
|
|
27
|
+
apiKey: "secret-key",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("resolveTemporalRuntimeConfigValues prefers explicit environment overrides", () => {
|
|
32
|
+
const resolved = resolveTemporalRuntimeConfigValues(
|
|
33
|
+
baseConfig,
|
|
34
|
+
{
|
|
35
|
+
TEMPORAL_ADDRESS: "env.temporal:7233",
|
|
36
|
+
TEMPORAL_NAMESPACE: "env.namespace",
|
|
37
|
+
TEMPORAL_TASK_QUEUE: "env-task-queue",
|
|
38
|
+
TEMPORAL_API_KEY: "env-key",
|
|
39
|
+
TEMPORAL_API_KEY_SECRET: "env-secret-name",
|
|
40
|
+
},
|
|
41
|
+
() => ({
|
|
42
|
+
address: "vault.temporal:7233",
|
|
43
|
+
namespace: "vault.namespace",
|
|
44
|
+
apiKey: "vault-key",
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(resolved.address).toBe("env.temporal:7233");
|
|
49
|
+
expect(resolved.namespace).toBe("env.namespace");
|
|
50
|
+
expect(resolved.taskQueue).toBe("env-task-queue");
|
|
51
|
+
expect(resolved.apiKey).toBe("env-key");
|
|
52
|
+
expect(resolved.apiKeySecretName).toBe("env-secret-name");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("resolveTemporalRuntimeConfigValues fails clearly when enabled Temporal resolves to localhost", () => {
|
|
56
|
+
expect(() => resolveTemporalRuntimeConfigValues(baseConfig, {}, () => ({}))).toThrow(
|
|
57
|
+
"Temporal is enabled for this Cloud Run service, but the resolved Temporal address is local"
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("resolveTemporalRuntimeConfigValues allows explicit Temporal disable", () => {
|
|
62
|
+
const resolved = resolveTemporalRuntimeConfigValues(baseConfig, { TEMPORAL_ENABLED: "false" }, () => ({}));
|
|
63
|
+
|
|
64
|
+
expect(resolved.enabled).toBeFalse();
|
|
65
|
+
expect(resolved.apiKeySecretName).toBe("");
|
|
66
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
type TemporalConfigInput = {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
address: string;
|
|
4
|
+
namespace: string;
|
|
5
|
+
taskQueue: string;
|
|
6
|
+
apiKeySecretName: string;
|
|
7
|
+
vaultMount: string;
|
|
8
|
+
vaultPath: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TemporalProviderFields = {
|
|
12
|
+
address?: string;
|
|
13
|
+
namespace?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type TemporalRuntimeConfig = {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
address: string;
|
|
20
|
+
namespace: string;
|
|
21
|
+
taskQueue: string;
|
|
22
|
+
apiKeySecretName: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function resolveTemporalRuntimeConfigValues(
|
|
27
|
+
config: TemporalConfigInput,
|
|
28
|
+
env: Record<string, string | undefined>,
|
|
29
|
+
readProviderFields: (mount: string, path: string) => TemporalProviderFields
|
|
30
|
+
): TemporalRuntimeConfig {
|
|
31
|
+
const enabledOverride = env.TEMPORAL_ENABLED?.trim();
|
|
32
|
+
const enabled = enabledOverride ? isTruthy(enabledOverride) : config.enabled;
|
|
33
|
+
const taskQueue = env.TEMPORAL_TASK_QUEUE?.trim() || config.taskQueue;
|
|
34
|
+
|
|
35
|
+
if (!enabled) {
|
|
36
|
+
return {
|
|
37
|
+
enabled: false,
|
|
38
|
+
address: env.TEMPORAL_ADDRESS?.trim() || config.address,
|
|
39
|
+
namespace: env.TEMPORAL_NAMESPACE?.trim() || config.namespace,
|
|
40
|
+
taskQueue,
|
|
41
|
+
apiKeySecretName: "",
|
|
42
|
+
apiKey: "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const provider = readProviderFields(config.vaultMount, config.vaultPath);
|
|
47
|
+
const address = env.TEMPORAL_ADDRESS?.trim() || provider.address || config.address;
|
|
48
|
+
const namespace = env.TEMPORAL_NAMESPACE?.trim() || provider.namespace || config.namespace;
|
|
49
|
+
const apiKey = env.TEMPORAL_API_KEY?.trim() || provider.apiKey || "";
|
|
50
|
+
const apiKeySecretName = env.TEMPORAL_API_KEY_SECRET?.trim() || (apiKey ? config.apiKeySecretName : "");
|
|
51
|
+
|
|
52
|
+
if (isLocalTemporalAddress(address)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
[
|
|
55
|
+
"Temporal is enabled for this Cloud Run service, but the resolved Temporal address is local.",
|
|
56
|
+
`Set TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, and TEMPORAL_API_KEY, or populate Vault at ${config.vaultMount}/${config.vaultPath}`,
|
|
57
|
+
"with TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, and TEMPORAL_API_KEY before running service create or service deploy.",
|
|
58
|
+
"Set TEMPORAL_ENABLED=false only for services that should deploy without Temporal.",
|
|
59
|
+
].join(" ")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!namespace) {
|
|
64
|
+
throw new Error(`Temporal is enabled but TEMPORAL_NAMESPACE is missing; set it in env or Vault at ${config.vaultMount}/${config.vaultPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
enabled,
|
|
69
|
+
address,
|
|
70
|
+
namespace,
|
|
71
|
+
taskQueue,
|
|
72
|
+
apiKeySecretName,
|
|
73
|
+
apiKey,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isTruthy(value: string) {
|
|
78
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isLocalTemporalAddress(address: string) {
|
|
82
|
+
const value = address.trim().toLowerCase();
|
|
83
|
+
return value === "" || value.startsWith("localhost:") || value.startsWith("127.0.0.1:") || value.startsWith("[::1]:");
|
|
84
|
+
}
|