@telorun/k8s-runner 0.6.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.
Files changed (63) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +206 -0
  3. package/dist/bundle-store.d.ts +32 -0
  4. package/dist/bundle-store.d.ts.map +1 -0
  5. package/dist/bundle-store.js +86 -0
  6. package/dist/bundle-store.js.map +1 -0
  7. package/dist/capabilities.d.ts +15 -0
  8. package/dist/capabilities.d.ts.map +1 -0
  9. package/dist/capabilities.js +30 -0
  10. package/dist/capabilities.js.map +1 -0
  11. package/dist/config.d.ts +95 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +76 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/k8s/backend.d.ts +19 -0
  16. package/dist/k8s/backend.d.ts.map +1 -0
  17. package/dist/k8s/backend.js +488 -0
  18. package/dist/k8s/backend.js.map +1 -0
  19. package/dist/k8s/client.d.ts +16 -0
  20. package/dist/k8s/client.d.ts.map +1 -0
  21. package/dist/k8s/client.js +24 -0
  22. package/dist/k8s/client.js.map +1 -0
  23. package/dist/k8s/image-build.d.ts +105 -0
  24. package/dist/k8s/image-build.d.ts.map +1 -0
  25. package/dist/k8s/image-build.js +432 -0
  26. package/dist/k8s/image-build.js.map +1 -0
  27. package/dist/k8s/ingress.d.ts +15 -0
  28. package/dist/k8s/ingress.d.ts.map +1 -0
  29. package/dist/k8s/ingress.js +102 -0
  30. package/dist/k8s/ingress.js.map +1 -0
  31. package/dist/k8s/pod-spec.d.ts +41 -0
  32. package/dist/k8s/pod-spec.d.ts.map +1 -0
  33. package/dist/k8s/pod-spec.js +146 -0
  34. package/dist/k8s/pod-spec.js.map +1 -0
  35. package/dist/limits.d.ts +25 -0
  36. package/dist/limits.d.ts.map +1 -0
  37. package/dist/limits.js +58 -0
  38. package/dist/limits.js.map +1 -0
  39. package/dist/server.d.ts +13 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +93 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/tar.d.ts +10 -0
  44. package/dist/tar.d.ts.map +1 -0
  45. package/dist/tar.js +63 -0
  46. package/dist/tar.js.map +1 -0
  47. package/package.json +41 -0
  48. package/src/bundle-store.ts +101 -0
  49. package/src/capabilities.ts +40 -0
  50. package/src/config.ts +201 -0
  51. package/src/k8s/backend-failure.test.ts +148 -0
  52. package/src/k8s/backend.ts +535 -0
  53. package/src/k8s/client.ts +39 -0
  54. package/src/k8s/image-build.test.ts +244 -0
  55. package/src/k8s/image-build.ts +547 -0
  56. package/src/k8s/ingress.test.ts +146 -0
  57. package/src/k8s/ingress.ts +127 -0
  58. package/src/k8s/pod-spec.ts +180 -0
  59. package/src/limits.test.ts +59 -0
  60. package/src/limits.ts +90 -0
  61. package/src/server.ts +124 -0
  62. package/src/tar.test.ts +55 -0
  63. package/src/tar.ts +68 -0
@@ -0,0 +1,244 @@
1
+ import type { RunBundle } from "@telorun/runner-core";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import type { ImageBuildConfig } from "../config.js";
5
+ import {
6
+ buildDockerfile,
7
+ buildKanikoJob,
8
+ computeImageTag,
9
+ imageRef,
10
+ parseAuthParams,
11
+ parseDockerConfigAuth,
12
+ registryHost,
13
+ } from "./image-build.js";
14
+
15
+ const APP_DOC = [
16
+ "kind: Telo.Application",
17
+ "metadata:",
18
+ " name: app",
19
+ "imports:",
20
+ " Console: std/console@0.9.0",
21
+ "---",
22
+ "kind: Telo.Definition",
23
+ "metadata:",
24
+ " name: Thing",
25
+ "controllers:",
26
+ " - pkg:npm/@acme/thing@1.0.0",
27
+ "",
28
+ ].join("\n");
29
+
30
+ const BUNDLE: RunBundle = {
31
+ entryRelativePath: "manifest.yaml",
32
+ files: [
33
+ { relativePath: "manifest.yaml", contents: APP_DOC },
34
+ { relativePath: "lib/util.yaml", contents: "kind: Telo.Library\nmetadata:\n name: util\n" },
35
+ ],
36
+ };
37
+
38
+ const BASE = "telorun/node:latest-slim";
39
+ const TELO_REGISTRY = "https://registry.telo.run";
40
+
41
+ function tagInputs(over: Partial<Parameters<typeof computeImageTag>[0]> = {}) {
42
+ return {
43
+ bundle: BUNDLE,
44
+ baseImage: BASE,
45
+ teloRegistryUrl: TELO_REGISTRY,
46
+ ...over,
47
+ };
48
+ }
49
+
50
+ function editApp(replace: string, withText: string): RunBundle {
51
+ return {
52
+ ...BUNDLE,
53
+ files: [
54
+ { relativePath: "manifest.yaml", contents: APP_DOC.replace(replace, withText) },
55
+ BUNDLE.files[1],
56
+ ],
57
+ };
58
+ }
59
+
60
+ describe("computeImageTag — dependency-closure addressing", () => {
61
+ it("is deterministic for identical inputs", () => {
62
+ expect(computeImageTag(tagInputs())).toBe(computeImageTag(tagInputs()));
63
+ });
64
+
65
+ it("is independent of file ordering in the bundle", () => {
66
+ const reversed: RunBundle = { ...BUNDLE, files: [...BUNDLE.files].reverse() };
67
+ expect(computeImageTag(tagInputs({ bundle: reversed }))).toBe(computeImageTag(tagInputs()));
68
+ });
69
+
70
+ it("ignores body-only edits that don't touch imports or controllers", () => {
71
+ const edited = editApp(" name: app", " name: app # renamed");
72
+ expect(computeImageTag(tagInputs({ bundle: edited }))).toBe(computeImageTag(tagInputs()));
73
+ });
74
+
75
+ it("changes when an import changes", () => {
76
+ const edited = editApp("std/console@0.9.0", "std/console@0.10.0");
77
+ expect(computeImageTag(tagInputs({ bundle: edited }))).not.toBe(computeImageTag(tagInputs()));
78
+ });
79
+
80
+ it("changes when a body-declared controller changes", () => {
81
+ const edited = editApp("pkg:npm/@acme/thing@1.0.0", "pkg:npm/@acme/thing@2.0.0");
82
+ expect(computeImageTag(tagInputs({ bundle: edited }))).not.toBe(computeImageTag(tagInputs()));
83
+ });
84
+
85
+ it("changes when the base image or telo registry change", () => {
86
+ const base = computeImageTag(tagInputs());
87
+ expect(computeImageTag(tagInputs({ baseImage: "telorun/node:next" }))).not.toBe(base);
88
+ expect(computeImageTag(tagInputs({ teloRegistryUrl: "https://other.example" }))).not.toBe(base);
89
+ });
90
+
91
+ it("produces a 32-char lowercase hex tag", () => {
92
+ expect(computeImageTag(tagInputs())).toMatch(/^[0-9a-f]{32}$/);
93
+ });
94
+
95
+ it("changes with the base digest so a moved moving-tag rebuilds", () => {
96
+ const noDigest = computeImageTag(tagInputs());
97
+ const pinned = computeImageTag(tagInputs({ baseDigest: "sha256:aaa" }));
98
+ const moved = computeImageTag(tagInputs({ baseDigest: "sha256:bbb" }));
99
+ expect(pinned).not.toBe(noDigest);
100
+ expect(moved).not.toBe(pinned);
101
+ });
102
+ });
103
+
104
+ describe("imageRef", () => {
105
+ it("joins repository and tag", () => {
106
+ const cfg = { repository: "reg.example/telo-sessions" } as ImageBuildConfig;
107
+ expect(imageRef(cfg, "abc123")).toBe("reg.example/telo-sessions:abc123");
108
+ });
109
+ });
110
+
111
+ describe("buildDockerfile", () => {
112
+ it("bakes the app and runs telo install at build time", () => {
113
+ const df = buildDockerfile({ baseImage: BASE, entryRelativePath: "app/manifest.yaml" });
114
+ expect(df).toContain(`FROM ${BASE}`);
115
+ expect(df).toContain("COPY . /app");
116
+ expect(df).toContain("ARG TELO_REGISTRY_URL");
117
+ expect(df).toContain("ENV TELO_CACHE_DIR=/telo-cache");
118
+ expect(df).toContain("RUN telo install /app/app/manifest.yaml");
119
+ });
120
+ });
121
+
122
+ const BUILD_CONFIG: ImageBuildConfig = {
123
+ repository: "reg.example/telo-sessions",
124
+ namespace: "telo-builds",
125
+ builderImage: "gcr.io/kaniko-project/executor:latest",
126
+ timeoutSeconds: 600,
127
+ insecureRegistry: false,
128
+ teloRegistryUrl: TELO_REGISTRY,
129
+ };
130
+
131
+ function kanikoArgsOf(job: ReturnType<typeof buildKanikoJob>): string[] {
132
+ return job.spec?.template?.spec?.containers?.[0]?.args ?? [];
133
+ }
134
+
135
+ describe("buildKanikoJob", () => {
136
+ const base = {
137
+ jobName: "telo-build-abc",
138
+ contextUrl: "http://runner.telo-runner:8062/internal/bundles/build-abc?token=xyz",
139
+ destination: "reg.example/telo-sessions:abc",
140
+ initImage: "busybox:stable",
141
+ managedByLabel: "telo-k8s-runner",
142
+ };
143
+
144
+ it("is a single-attempt, deadline-bounded, self-cleaning Job", () => {
145
+ const job = buildKanikoJob({ config: BUILD_CONFIG, ...base });
146
+ expect(job.spec?.backoffLimit).toBe(0);
147
+ expect(job.spec?.activeDeadlineSeconds).toBe(600);
148
+ expect(job.spec?.ttlSecondsAfterFinished).toBe(300);
149
+ expect(job.spec?.template?.spec?.restartPolicy).toBe("Never");
150
+ expect(job.spec?.template?.spec?.automountServiceAccountToken).toBe(false);
151
+ expect(job.metadata?.namespace).toBe("telo-builds");
152
+ });
153
+
154
+ it("fetches the tokenized context in an initContainer", () => {
155
+ const job = buildKanikoJob({ config: BUILD_CONFIG, ...base });
156
+ const init = job.spec?.template?.spec?.initContainers?.[0];
157
+ expect(init?.image).toBe("busybox:stable");
158
+ expect(init?.args?.[0]).toContain(base.contextUrl);
159
+ });
160
+
161
+ it("passes context, dockerfile, destination and the telo registry build-arg", () => {
162
+ const args = kanikoArgsOf(buildKanikoJob({ config: BUILD_CONFIG, ...base }));
163
+ expect(args).toContain("--context=dir:///workspace");
164
+ expect(args).toContain("--dockerfile=/workspace/Dockerfile");
165
+ expect(args).toContain(`--destination=${base.destination}`);
166
+ expect(args).toContain(`--build-arg=TELO_REGISTRY_URL=${TELO_REGISTRY}`);
167
+ });
168
+
169
+ it("omits insecure flags by default and includes them when configured", () => {
170
+ expect(kanikoArgsOf(buildKanikoJob({ config: BUILD_CONFIG, ...base }))).not.toContain("--insecure");
171
+ const insecure = kanikoArgsOf(
172
+ buildKanikoJob({ config: { ...BUILD_CONFIG, insecureRegistry: true }, ...base }),
173
+ );
174
+ expect(insecure).toContain("--insecure");
175
+ expect(insecure).toContain("--skip-tls-verify");
176
+ });
177
+
178
+ it("mounts a docker-config secret only when a push secret is set", () => {
179
+ const without = buildKanikoJob({ config: BUILD_CONFIG, ...base });
180
+ expect(without.spec?.template?.spec?.volumes?.some((v) => v.name === "docker-config")).toBe(false);
181
+
182
+ const withSecret = buildKanikoJob({
183
+ config: { ...BUILD_CONFIG, pushSecretName: "reg-creds" },
184
+ ...base,
185
+ });
186
+ const vol = withSecret.spec?.template?.spec?.volumes?.find((v) => v.name === "docker-config");
187
+ expect(vol?.secret?.secretName).toBe("reg-creds");
188
+ expect(
189
+ withSecret.spec?.template?.spec?.containers?.[0]?.volumeMounts?.some(
190
+ (m) => m.name === "docker-config",
191
+ ),
192
+ ).toBe(true);
193
+ });
194
+ });
195
+
196
+ describe("registryHost", () => {
197
+ it("strips scheme and path to the bare host", () => {
198
+ expect(registryHost("registry.example.com/telo-sessions")).toBe("registry.example.com");
199
+ expect(registryHost("https://registry.example.com/v2/")).toBe("registry.example.com");
200
+ expect(registryHost("registry.example.com:5000/team/app")).toBe("registry.example.com:5000");
201
+ });
202
+ });
203
+
204
+ describe("parseAuthParams", () => {
205
+ it("parses realm, service and scope from a Bearer challenge", () => {
206
+ const params = parseAuthParams(
207
+ 'realm="https://auth.example.com/token",service="reg.example.com",scope="repository:team/app:pull"',
208
+ );
209
+ expect(params.realm).toBe("https://auth.example.com/token");
210
+ expect(params.service).toBe("reg.example.com");
211
+ expect(params.scope).toBe("repository:team/app:pull");
212
+ });
213
+ });
214
+
215
+ describe("parseDockerConfigAuth", () => {
216
+ const REPO = "registry.example.com/telo-sessions";
217
+
218
+ it("returns explicit username/password for the repo's host", () => {
219
+ const json = JSON.stringify({
220
+ auths: { "registry.example.com": { username: "alice", password: "s3cret" } },
221
+ });
222
+ expect(parseDockerConfigAuth(json, REPO)).toEqual({ username: "alice", password: "s3cret" });
223
+ });
224
+
225
+ it("decodes the base64 `auth` field when username/password are absent", () => {
226
+ const auth = Buffer.from("bob:hunter2").toString("base64");
227
+ const json = JSON.stringify({ auths: { "registry.example.com": { auth } } });
228
+ expect(parseDockerConfigAuth(json, REPO)).toEqual({ username: "bob", password: "hunter2" });
229
+ });
230
+
231
+ it("matches a host even when the dockerconfig key carries a scheme", () => {
232
+ const json = JSON.stringify({
233
+ auths: { "https://registry.example.com/v2/": { username: "carol", password: "pw" } },
234
+ });
235
+ expect(parseDockerConfigAuth(json, REPO)).toEqual({ username: "carol", password: "pw" });
236
+ });
237
+
238
+ it("returns undefined when no entry matches the host or the json is invalid", () => {
239
+ expect(
240
+ parseDockerConfigAuth(JSON.stringify({ auths: { "other.io": { auth: "x" } } }), REPO),
241
+ ).toBeUndefined();
242
+ expect(parseDockerConfigAuth("not json", REPO)).toBeUndefined();
243
+ });
244
+ });