@esuyo/coding-container 1.0.2

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.
@@ -0,0 +1,162 @@
1
+ import { exec, runWithInput } from "./exec.js";
2
+ import { isSemver, compareSemver } from "./semver.js";
3
+
4
+ /**
5
+ * The team-wide source of truth for the global coding image: one ConfigMap
6
+ * in the cluster, no files on laptops. All team state lives here; laptops
7
+ * hold only machine secrets (docker login, kubectl context).
8
+ *
9
+ * Data keys (all strings):
10
+ * current - what global-mode deploys float on
11
+ * previous - last good version, the default rollback target (may be absent)
12
+ * maxEver - highest version ever published; never moves backward, so a
13
+ * rolled-back (poisoned) tag can never be republished
14
+ * keep - gc window: how many versions behind current to keep
15
+ */
16
+ export const SYSTEM_CM_NAME = "coding-system";
17
+ export const SYSTEM_CM_LABEL = "coding-container=true";
18
+
19
+ export const INIT_HINT =
20
+ `The ${SYSTEM_CM_NAME} ConfigMap does not exist yet. Bootstrap it with:\n` +
21
+ ` coding-container system init --tag v1.0.0 --keep 10`;
22
+
23
+ /**
24
+ * Which kubectl to run. KUBECTL_BIN lets tests (and users with a wrapper)
25
+ * point at a specific binary. A node script stub (.js/.cjs/.mjs) is run
26
+ * directly, without a shell: piping stdin through cmd.exe is racy on
27
+ * Windows (the apply -f - manifest would be lost). A Windows script stub
28
+ * (.cmd/.bat) needs a shell to spawn.
29
+ * Returns { cmd, args, opts }; `args` must be prepended to the kubectl args.
30
+ */
31
+ export function kubectlExec() {
32
+ const bin = process.env.KUBECTL_BIN || "kubectl";
33
+ if (process.env.KUBECTL_BIN && /\.(c|m)?js$/.test(bin)) {
34
+ return { cmd: process.execPath, args: [bin], opts: {} };
35
+ }
36
+ const opts =
37
+ process.env.KUBECTL_BIN && process.platform === "win32" && /\.(cmd|bat)$/i.test(bin)
38
+ ? { shell: true }
39
+ : {};
40
+ return { cmd: bin, args: [], opts };
41
+ }
42
+
43
+ /**
44
+ * Parse and validate the data block of the coding-system ConfigMap.
45
+ * Throws with a clear message on any invalid value.
46
+ */
47
+ export function parseSystemData(data) {
48
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
49
+ throw new Error(`system config: ${SYSTEM_CM_NAME} ConfigMap has no data block`);
50
+ }
51
+ const out = {};
52
+ for (const key of ["current", "maxEver"]) {
53
+ const v = data[key];
54
+ if (typeof v !== "string" || !isSemver(v)) {
55
+ throw new Error(
56
+ `system config: "${key}" must be a semver string (vX.Y.Z), got: ${JSON.stringify(v)}`
57
+ );
58
+ }
59
+ out[key] = v;
60
+ }
61
+ const previous = data.previous;
62
+ if (previous === undefined || previous === "") {
63
+ out.previous = undefined;
64
+ } else if (!isSemver(previous)) {
65
+ throw new Error(
66
+ `system config: "previous" must be a semver string (vX.Y.Z) or empty, got: ${JSON.stringify(previous)}`
67
+ );
68
+ } else {
69
+ out.previous = previous;
70
+ }
71
+ const keep = data.keep;
72
+ if (typeof keep !== "string" || !/^\d+$/.test(keep)) {
73
+ throw new Error(
74
+ `system config: "keep" must be a non-negative integer string, got: ${JSON.stringify(keep)}`
75
+ );
76
+ }
77
+ out.keep = Number.parseInt(keep, 10);
78
+ if (compareSemver(out.current, out.maxEver) > 0) {
79
+ throw new Error(
80
+ `system config: current ${out.current} is newer than maxEver ${out.maxEver} — the ConfigMap is corrupted`
81
+ );
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /**
87
+ * Read the coding-system ConfigMap from the cluster. Throws INIT_HINT when
88
+ * the ConfigMap is missing (fresh cluster) so callers can surface it.
89
+ */
90
+ export function getSystemConfig(ns) {
91
+ const { cmd, args, opts } = kubectlExec();
92
+ let out;
93
+ try {
94
+ out = exec(cmd, [...args, "get", "cm", SYSTEM_CM_NAME, "-n", ns, "-o", "json"], opts);
95
+ } catch (err) {
96
+ if (/not found/i.test(err.message)) {
97
+ throw new Error(INIT_HINT);
98
+ }
99
+ throw new Error(
100
+ `cannot read the ${SYSTEM_CM_NAME} ConfigMap in namespace ${ns}:\n${err.message}`
101
+ );
102
+ }
103
+ let cm;
104
+ try {
105
+ cm = JSON.parse(out);
106
+ } catch (err) {
107
+ throw new Error(`cannot parse the ${SYSTEM_CM_NAME} ConfigMap: ${err.message}`);
108
+ }
109
+ return parseSystemData(cm.data);
110
+ }
111
+
112
+ export function systemConfigManifest(ns, data) {
113
+ return {
114
+ apiVersion: "v1",
115
+ kind: "ConfigMap",
116
+ metadata: {
117
+ name: SYSTEM_CM_NAME,
118
+ namespace: ns,
119
+ labels: { "coding-container": "true" },
120
+ },
121
+ data,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Write the full coding-system state (create-or-update via kubectl apply).
127
+ * Callers compute the new complete state; the ConfigMap is small and the
128
+ * writers are human-driven, so read-then-write is safe for this team.
129
+ */
130
+ export async function patchSystemConfig(ns, { current, previous, maxEver, keep }) {
131
+ const data = { current, maxEver, keep: String(keep) };
132
+ if (previous) data.previous = previous;
133
+ parseSystemData(data);
134
+ const { cmd, args, opts } = kubectlExec();
135
+ await runWithInput(cmd, [...args, "apply", "-f", "-"], JSON.stringify(systemConfigManifest(ns, data)), opts);
136
+ }
137
+
138
+ /**
139
+ * The tag a deploy/create should use:
140
+ * custom mode - the project's own pin (image.tag or --tag), required
141
+ * global mode - the project's pin when set (image.tag or --tag), else
142
+ * the ConfigMap's current (the float)
143
+ */
144
+ export function resolveDeployTag(cfg) {
145
+ if (cfg.imageMode === "custom") {
146
+ if (!cfg.image.tag) {
147
+ throw new Error(
148
+ "custom image mode requires a tag: set image.tag in the project config or pass --tag"
149
+ );
150
+ }
151
+ return cfg.image.tag;
152
+ }
153
+ if (cfg.image.tag) {
154
+ if (!isSemver(cfg.image.tag)) {
155
+ throw new Error(
156
+ `global-mode tags must be semver (vX.Y.Z) — the global repo only carries semver tags, got: ${cfg.image.tag}`
157
+ );
158
+ }
159
+ return cfg.image.tag;
160
+ }
161
+ return getSystemConfig(cfg.k8s.namespace).current;
162
+ }