@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.
- package/LICENSE +17 -0
- package/README.md +206 -0
- package/dist/bundle-store.d.ts +32 -0
- package/dist/bundle-store.d.ts.map +1 -0
- package/dist/bundle-store.js +86 -0
- package/dist/bundle-store.js.map +1 -0
- package/dist/capabilities.d.ts +15 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +30 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/config.d.ts +95 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +76 -0
- package/dist/config.js.map +1 -0
- package/dist/k8s/backend.d.ts +19 -0
- package/dist/k8s/backend.d.ts.map +1 -0
- package/dist/k8s/backend.js +488 -0
- package/dist/k8s/backend.js.map +1 -0
- package/dist/k8s/client.d.ts +16 -0
- package/dist/k8s/client.d.ts.map +1 -0
- package/dist/k8s/client.js +24 -0
- package/dist/k8s/client.js.map +1 -0
- package/dist/k8s/image-build.d.ts +105 -0
- package/dist/k8s/image-build.d.ts.map +1 -0
- package/dist/k8s/image-build.js +432 -0
- package/dist/k8s/image-build.js.map +1 -0
- package/dist/k8s/ingress.d.ts +15 -0
- package/dist/k8s/ingress.d.ts.map +1 -0
- package/dist/k8s/ingress.js +102 -0
- package/dist/k8s/ingress.js.map +1 -0
- package/dist/k8s/pod-spec.d.ts +41 -0
- package/dist/k8s/pod-spec.d.ts.map +1 -0
- package/dist/k8s/pod-spec.js +146 -0
- package/dist/k8s/pod-spec.js.map +1 -0
- package/dist/limits.d.ts +25 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +58 -0
- package/dist/limits.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/dist/tar.d.ts +10 -0
- package/dist/tar.d.ts.map +1 -0
- package/dist/tar.js +63 -0
- package/dist/tar.js.map +1 -0
- package/package.json +41 -0
- package/src/bundle-store.ts +101 -0
- package/src/capabilities.ts +40 -0
- package/src/config.ts +201 -0
- package/src/k8s/backend-failure.test.ts +148 -0
- package/src/k8s/backend.ts +535 -0
- package/src/k8s/client.ts +39 -0
- package/src/k8s/image-build.test.ts +244 -0
- package/src/k8s/image-build.ts +547 -0
- package/src/k8s/ingress.test.ts +146 -0
- package/src/k8s/ingress.ts +127 -0
- package/src/k8s/pod-spec.ts +180 -0
- package/src/limits.test.ts +59 -0
- package/src/limits.ts +90 -0
- package/src/server.ts +124 -0
- package/src/tar.test.ts +55 -0
- package/src/tar.ts +68 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadCoreConfig,
|
|
3
|
+
parseBool,
|
|
4
|
+
parsePositiveInt,
|
|
5
|
+
RunnerConfigError,
|
|
6
|
+
type RunnerCoreConfig,
|
|
7
|
+
type TagFilter,
|
|
8
|
+
} from "@telorun/runner-core";
|
|
9
|
+
|
|
10
|
+
export { RunnerConfigError };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base-image catalog settings. The runner resolves the menu of base images a
|
|
14
|
+
* session may pick from `telorun/node`'s Docker Hub tags (configurable repo),
|
|
15
|
+
* filtered to taste, and advertises it as an editable `image` picker. The
|
|
16
|
+
* configured `defaultImage` is always offered (and is the fallback when Docker
|
|
17
|
+
* Hub is unreachable). Disable to lock `image` to `defaultImage` (the old
|
|
18
|
+
* server-enforced behaviour) — e.g. an air-gapped cluster.
|
|
19
|
+
*/
|
|
20
|
+
export interface BaseImageCatalogConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** `namespace/repository` queried on Docker Hub, e.g. `telorun/node`. */
|
|
23
|
+
repository: string;
|
|
24
|
+
filter: TagFilter;
|
|
25
|
+
/** Cap on advertised tags (newest first). */
|
|
26
|
+
limit: number;
|
|
27
|
+
/** Background refresh cadence in ms. */
|
|
28
|
+
refreshIntervalMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resource limits the runner will ever grant. These are HARD CEILINGS, not
|
|
33
|
+
* defaults a request can exceed: the effective per-session value is
|
|
34
|
+
* `min(requested, ceiling)` (clamp-down only). For a bare runner serving an
|
|
35
|
+
* anonymous tier directly, the ceiling IS the policy.
|
|
36
|
+
*/
|
|
37
|
+
export interface LimitCeilings {
|
|
38
|
+
/** Kubernetes CPU quantity, e.g. "50m". */
|
|
39
|
+
cpu: string;
|
|
40
|
+
/** Kubernetes memory quantity, e.g. "100Mi". */
|
|
41
|
+
memory: string;
|
|
42
|
+
/** Wall-clock TTL in seconds (Pod activeDeadlineSeconds). */
|
|
43
|
+
ttlSeconds: number;
|
|
44
|
+
/** Per-Pod ephemeral-storage limit, e.g. "512Mi". */
|
|
45
|
+
ephemeralStorage: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* On-cluster image-build settings. The runner ALWAYS prebuilds a self-contained
|
|
50
|
+
* per-app image (controllers + module manifests baked in via `telo install`) and
|
|
51
|
+
* the session pod runs it directly — there is no in-pod install fallback, so a
|
|
52
|
+
* registry repository (`RUNNER_IMAGE_REPOSITORY`) is required.
|
|
53
|
+
*/
|
|
54
|
+
export interface ImageBuildConfig {
|
|
55
|
+
/** Registry repository the per-app image is pushed to / pulled from; the tag
|
|
56
|
+
* is the bundle content hash (e.g. `registry.telo-runner.svc:5000/telo-sessions`). */
|
|
57
|
+
repository: string;
|
|
58
|
+
/** Namespace the trusted Kaniko build Jobs run in (registry egress lives here). */
|
|
59
|
+
namespace: string;
|
|
60
|
+
/** Builder image — Kaniko (or a compatible `--context`/`--destination` executor). */
|
|
61
|
+
builderImage: string;
|
|
62
|
+
/** Build Job `activeDeadlineSeconds` and the runner's wait budget. */
|
|
63
|
+
timeoutSeconds: number;
|
|
64
|
+
/** Push/pull to an insecure (HTTP / self-signed) registry — the in-cluster default. */
|
|
65
|
+
insecureRegistry: boolean;
|
|
66
|
+
/** HTTP(S) base for a best-effort manifest existence check (skip build on hit);
|
|
67
|
+
* undefined → always build. */
|
|
68
|
+
registryApiUrl?: string;
|
|
69
|
+
/** Telo module registry `telo install` resolves manifests from during the build. */
|
|
70
|
+
teloRegistryUrl: string;
|
|
71
|
+
/** Optional dockerconfig Secret (in the build namespace) Kaniko pushes with. */
|
|
72
|
+
pushSecretName?: string;
|
|
73
|
+
/** Optional dockerconfig Secret (in the session namespace) the kubelet uses to
|
|
74
|
+
* pull per-app images from a private registry. */
|
|
75
|
+
imagePullSecret?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface K8sRunnerConfig extends RunnerCoreConfig {
|
|
79
|
+
/** Namespace where session Pods/Services/Ingresses are created. */
|
|
80
|
+
sessionNamespace: string;
|
|
81
|
+
/** Default image for spawned session Pods (telorun/node). */
|
|
82
|
+
defaultImage: string;
|
|
83
|
+
/** Small image for the bundle/build-context fetch initContainer (needs wget + tar). */
|
|
84
|
+
initImage: string;
|
|
85
|
+
/** Optional sandbox RuntimeClass (gvisor/kata). Unset → cluster default (runc). */
|
|
86
|
+
runtimeClass?: string;
|
|
87
|
+
/** Wildcard base domain for per-session ingress; unset → logs-only. */
|
|
88
|
+
sessionIngressBaseDomain?: string;
|
|
89
|
+
/** Optional IngressClass name for created session Ingresses. */
|
|
90
|
+
sessionIngressClassName?: string;
|
|
91
|
+
/** Optional `kubernetes.io/tls` Secret (in the session namespace) the per-session
|
|
92
|
+
* Ingress presents so an upstream (e.g. Cloudflare Full (Strict)) can validate
|
|
93
|
+
* the origin. Must cover the wildcard `*.<sessionIngressBaseDomain>`. Unset → no TLS block. */
|
|
94
|
+
sessionIngressTlsSecretName?: string;
|
|
95
|
+
/** On-cluster image build — the only delivery path (always present). */
|
|
96
|
+
build: ImageBuildConfig;
|
|
97
|
+
/** Runner's own in-cluster base URL, used to build the bundle/build-context
|
|
98
|
+
* fetch URL the initContainer curls (e.g. http://k8s-runner.telo-runner:8062). */
|
|
99
|
+
selfUrl: string;
|
|
100
|
+
/** Label applied to every session object, used for orphan reaping. */
|
|
101
|
+
managedByLabel: string;
|
|
102
|
+
limits: LimitCeilings;
|
|
103
|
+
/** Menu of base images a session may pick (advertised as the `image` enum). */
|
|
104
|
+
baseImageCatalog: BaseImageCatalogConfig;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const DEFAULT_PORT = 8062;
|
|
108
|
+
|
|
109
|
+
/** Compile a single optional regex env var into a one-element `RegExp[]`. */
|
|
110
|
+
function parseTagRegex(raw: string | undefined, field: string): RegExp[] | undefined {
|
|
111
|
+
const v = raw?.trim();
|
|
112
|
+
if (!v) return undefined;
|
|
113
|
+
try {
|
|
114
|
+
return [new RegExp(v)];
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new RunnerConfigError(
|
|
117
|
+
`${field} is not a valid regular expression: ${(err as Error).message}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadBaseImageCatalogConfig(env: NodeJS.ProcessEnv): BaseImageCatalogConfig {
|
|
123
|
+
return {
|
|
124
|
+
enabled: parseBool(env.RUNNER_BASE_IMAGE_CATALOG_ENABLED, true, "RUNNER_BASE_IMAGE_CATALOG_ENABLED"),
|
|
125
|
+
repository: env.RUNNER_BASE_IMAGE_REPO?.trim() || "telorun/node",
|
|
126
|
+
filter: {
|
|
127
|
+
pinnedOnly: parseBool(env.RUNNER_BASE_IMAGE_PINNED_ONLY, true, "RUNNER_BASE_IMAGE_PINNED_ONLY"),
|
|
128
|
+
excludeSha: parseBool(env.RUNNER_BASE_IMAGE_EXCLUDE_SHA, true, "RUNNER_BASE_IMAGE_EXCLUDE_SHA"),
|
|
129
|
+
excludePrerelease: parseBool(
|
|
130
|
+
env.RUNNER_BASE_IMAGE_EXCLUDE_PRERELEASE,
|
|
131
|
+
true,
|
|
132
|
+
"RUNNER_BASE_IMAGE_EXCLUDE_PRERELEASE",
|
|
133
|
+
),
|
|
134
|
+
include: parseTagRegex(env.RUNNER_BASE_IMAGE_INCLUDE, "RUNNER_BASE_IMAGE_INCLUDE"),
|
|
135
|
+
exclude: parseTagRegex(env.RUNNER_BASE_IMAGE_EXCLUDE, "RUNNER_BASE_IMAGE_EXCLUDE"),
|
|
136
|
+
},
|
|
137
|
+
limit: parsePositiveInt(env.RUNNER_BASE_IMAGE_LIMIT, 20, "RUNNER_BASE_IMAGE_LIMIT"),
|
|
138
|
+
refreshIntervalMs:
|
|
139
|
+
parsePositiveInt(
|
|
140
|
+
env.RUNNER_BASE_IMAGE_REFRESH_SECONDS,
|
|
141
|
+
3600,
|
|
142
|
+
"RUNNER_BASE_IMAGE_REFRESH_SECONDS",
|
|
143
|
+
) * 1000,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function loadK8sRunnerConfig(env: NodeJS.ProcessEnv): K8sRunnerConfig {
|
|
148
|
+
const selfUrl = env.RUNNER_SELF_URL?.trim();
|
|
149
|
+
if (!selfUrl) {
|
|
150
|
+
throw new RunnerConfigError(
|
|
151
|
+
"RUNNER_SELF_URL env var is required. Set it to the runner's in-cluster base URL " +
|
|
152
|
+
"(e.g. http://k8s-runner.telo-runner.svc:8062) so session initContainers can fetch the bundle.",
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const repository = env.RUNNER_IMAGE_REPOSITORY?.trim();
|
|
157
|
+
if (!repository) {
|
|
158
|
+
throw new RunnerConfigError(
|
|
159
|
+
"RUNNER_IMAGE_REPOSITORY env var is required. The runner prebuilds a per-app session image for " +
|
|
160
|
+
"every run (there is no in-pod install fallback); set it to the registry repository the built " +
|
|
161
|
+
"images are pushed to / pulled from (e.g. registry.example.com/telo-sessions). The tag is the " +
|
|
162
|
+
"bundle content hash.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const build: ImageBuildConfig = {
|
|
166
|
+
repository: repository.replace(/\/+$/, ""),
|
|
167
|
+
namespace: env.RUNNER_BUILD_NAMESPACE?.trim() || "telo-builds",
|
|
168
|
+
builderImage: env.RUNNER_BUILDER_IMAGE?.trim() || "gcr.io/kaniko-project/executor:latest",
|
|
169
|
+
timeoutSeconds: parsePositiveInt(
|
|
170
|
+
env.RUNNER_BUILD_TIMEOUT_SECONDS,
|
|
171
|
+
600,
|
|
172
|
+
"RUNNER_BUILD_TIMEOUT_SECONDS",
|
|
173
|
+
),
|
|
174
|
+
insecureRegistry: env.RUNNER_REGISTRY_INSECURE?.trim() === "true",
|
|
175
|
+
registryApiUrl: env.RUNNER_REGISTRY_API_URL?.trim() || undefined,
|
|
176
|
+
teloRegistryUrl: env.TELO_REGISTRY_URL?.trim() || "https://registry.telo.run",
|
|
177
|
+
pushSecretName: env.RUNNER_REGISTRY_PUSH_SECRET?.trim() || undefined,
|
|
178
|
+
imagePullSecret: env.RUNNER_IMAGE_PULL_SECRET?.trim() || undefined,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...loadCoreConfig(env, { port: DEFAULT_PORT }),
|
|
183
|
+
sessionNamespace: env.RUNNER_SESSION_NAMESPACE?.trim() || "telo-sessions",
|
|
184
|
+
defaultImage: env.RUNNER_IMAGE?.trim() || "telorun/node:latest-slim",
|
|
185
|
+
initImage: env.RUNNER_INIT_IMAGE?.trim() || "busybox:stable",
|
|
186
|
+
runtimeClass: env.RUNNER_RUNTIME_CLASS?.trim() || undefined,
|
|
187
|
+
sessionIngressBaseDomain: env.SESSION_INGRESS_BASE_DOMAIN?.trim() || undefined,
|
|
188
|
+
sessionIngressClassName: env.SESSION_INGRESS_CLASS?.trim() || undefined,
|
|
189
|
+
sessionIngressTlsSecretName: env.SESSION_INGRESS_TLS_SECRET?.trim() || undefined,
|
|
190
|
+
build,
|
|
191
|
+
selfUrl: selfUrl.replace(/\/+$/, ""),
|
|
192
|
+
managedByLabel: env.RUNNER_MANAGED_BY?.trim() || "telo-k8s-runner",
|
|
193
|
+
limits: {
|
|
194
|
+
cpu: env.RUNNER_MAX_CPU?.trim() || "50m",
|
|
195
|
+
memory: env.RUNNER_MAX_MEMORY?.trim() || "100Mi",
|
|
196
|
+
ttlSeconds: parsePositiveInt(env.RUNNER_MAX_TTL_SECONDS, 3600, "RUNNER_MAX_TTL_SECONDS"),
|
|
197
|
+
ephemeralStorage: env.RUNNER_MAX_EPHEMERAL_STORAGE?.trim() || "512Mi",
|
|
198
|
+
},
|
|
199
|
+
baseImageCatalog: loadBaseImageCatalogConfig(env),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { V1Pod } from "@kubernetes/client-node";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { podFailureMessage } from "./backend.js";
|
|
5
|
+
|
|
6
|
+
function pod(status: V1Pod["status"]): V1Pod {
|
|
7
|
+
return { apiVersion: "v1", kind: "Pod", status };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("podFailureMessage — surfaces the real cause", () => {
|
|
11
|
+
it("reports a failed init container (the bundle-fetch case)", () => {
|
|
12
|
+
const msg = podFailureMessage(
|
|
13
|
+
pod({
|
|
14
|
+
phase: "Failed",
|
|
15
|
+
initContainerStatuses: [
|
|
16
|
+
{
|
|
17
|
+
name: "bundle-fetch",
|
|
18
|
+
image: "busybox",
|
|
19
|
+
imageID: "",
|
|
20
|
+
ready: false,
|
|
21
|
+
restartCount: 0,
|
|
22
|
+
state: { terminated: { exitCode: 1, reason: "Error" } },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
expect(msg).toBe('init container "bundle-fetch" failed: Error (exit code 1)');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("includes the terminated message when present", () => {
|
|
31
|
+
const msg = podFailureMessage(
|
|
32
|
+
pod({
|
|
33
|
+
phase: "Failed",
|
|
34
|
+
initContainerStatuses: [
|
|
35
|
+
{
|
|
36
|
+
name: "bundle-fetch",
|
|
37
|
+
image: "busybox",
|
|
38
|
+
imageID: "",
|
|
39
|
+
ready: false,
|
|
40
|
+
restartCount: 0,
|
|
41
|
+
state: { terminated: { exitCode: 1, reason: "Error", message: "wget: download timed out" } },
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
expect(msg).toBe('init container "bundle-fetch" failed: Error (exit code 1): wget: download timed out');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("surfaces a blocking waiting reason like ImagePullBackOff", () => {
|
|
50
|
+
const msg = podFailureMessage(
|
|
51
|
+
pod({
|
|
52
|
+
phase: "Pending",
|
|
53
|
+
initContainerStatuses: [
|
|
54
|
+
{
|
|
55
|
+
name: "bundle-fetch",
|
|
56
|
+
image: "busybox",
|
|
57
|
+
imageID: "",
|
|
58
|
+
ready: false,
|
|
59
|
+
restartCount: 0,
|
|
60
|
+
state: { waiting: { reason: "ImagePullBackOff", message: "Back-off pulling image" } },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
expect(msg).toBe('init container "bundle-fetch" waiting: ImagePullBackOff: Back-off pulling image');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("skips a succeeded init container and reports the failing main container", () => {
|
|
69
|
+
const msg = podFailureMessage(
|
|
70
|
+
pod({
|
|
71
|
+
phase: "Failed",
|
|
72
|
+
initContainerStatuses: [
|
|
73
|
+
{
|
|
74
|
+
name: "bundle-fetch",
|
|
75
|
+
image: "busybox",
|
|
76
|
+
imageID: "",
|
|
77
|
+
ready: true,
|
|
78
|
+
restartCount: 0,
|
|
79
|
+
state: { terminated: { exitCode: 0, reason: "Completed" } },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
containerStatuses: [
|
|
83
|
+
{
|
|
84
|
+
name: "session",
|
|
85
|
+
image: "telo",
|
|
86
|
+
imageID: "",
|
|
87
|
+
ready: false,
|
|
88
|
+
restartCount: 0,
|
|
89
|
+
state: { terminated: { exitCode: 137, reason: "OOMKilled" } },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
expect(msg).toBe('container "session" failed: OOMKilled (exit code 137)');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ignores benign transient waiting reasons", () => {
|
|
98
|
+
const msg = podFailureMessage(
|
|
99
|
+
pod({
|
|
100
|
+
phase: "Failed",
|
|
101
|
+
reason: "DeadlineExceeded",
|
|
102
|
+
message: "Pod was active on the node longer than the specified deadline",
|
|
103
|
+
containerStatuses: [
|
|
104
|
+
{
|
|
105
|
+
name: "session",
|
|
106
|
+
image: "telo",
|
|
107
|
+
imageID: "",
|
|
108
|
+
ready: false,
|
|
109
|
+
restartCount: 0,
|
|
110
|
+
state: { waiting: { reason: "PodInitializing" } },
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
expect(msg).toBe("Pod was active on the node longer than the specified deadline");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("falls back to pod-level reason when no container detail exists", () => {
|
|
119
|
+
const msg = podFailureMessage(pod({ phase: "Failed", reason: "Evicted" }));
|
|
120
|
+
expect(msg).toBe("Evicted");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns the bare fallback only when nothing is known", () => {
|
|
124
|
+
expect(podFailureMessage(pod({ phase: "Failed" }))).toBe("pod failed");
|
|
125
|
+
expect(podFailureMessage(undefined)).toBe("pod failed");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("truncates an oversized detail message", () => {
|
|
129
|
+
const long = "x".repeat(800);
|
|
130
|
+
const msg = podFailureMessage(
|
|
131
|
+
pod({
|
|
132
|
+
phase: "Failed",
|
|
133
|
+
containerStatuses: [
|
|
134
|
+
{
|
|
135
|
+
name: "session",
|
|
136
|
+
image: "telo",
|
|
137
|
+
imageID: "",
|
|
138
|
+
ready: false,
|
|
139
|
+
restartCount: 0,
|
|
140
|
+
state: { terminated: { exitCode: 1, message: long } },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
expect(msg.endsWith("…")).toBe(true);
|
|
146
|
+
expect(msg.length).toBeLessThan(560);
|
|
147
|
+
});
|
|
148
|
+
});
|