feat(openclaw): per-tenant tag override + platform default ConfigMap (tag-only)
All checks were successful
Build and Push / build (push) Successful in 1m52s
All checks were successful
Build and Push / build (push) Successful in 1m52s
This commit is contained in:
112
src/lib/k8s.ts
112
src/lib/k8s.ts
@@ -173,3 +173,115 @@ export async function setTenantAnnotation(
|
||||
}
|
||||
return res.json() as Promise<PiecedTenant>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenClaw config ConfigMap helpers (admin-only feature: per-tenant version
|
||||
// override + platform default).
|
||||
//
|
||||
// The ConfigMap lives in the operator's namespace (`pieced-system`). The
|
||||
// portal's ServiceAccount needs `get/patch` on configmaps in that namespace
|
||||
// — rules added in the gitops repo.
|
||||
//
|
||||
// Tag-only by design — see operator notes for rationale.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const OPENCLAW_CONFIGMAP_NAME = "pieced-openclaw-config";
|
||||
|
||||
/**
|
||||
* Operator namespace. Reads the env var so the portal can be deployed in
|
||||
* non-default namespaces without code changes; defaults to "pieced-system"
|
||||
* matching the operator's chart default.
|
||||
*/
|
||||
function getOperatorNamespace(): string {
|
||||
return process.env.OPERATOR_NAMESPACE ?? "pieced-system";
|
||||
}
|
||||
|
||||
export interface OpenClawDefaults {
|
||||
/** Image tag (e.g. "2026.4.22"). Empty string means unset. */
|
||||
defaultTag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the platform-default OpenClaw image tag. Returns empty string
|
||||
* if unset, and `{ defaultTag: "" }` if the ConfigMap doesn't exist yet
|
||||
* — the operator's built-in fallback is invisible to the portal by
|
||||
* design (we don't want the UI to claim "current default: 2026.x" when
|
||||
* it's actually the operator binary's baked-in version; that would be
|
||||
* misleading once the binary updates).
|
||||
*/
|
||||
export async function getOpenClawDefaults(): Promise<OpenClawDefaults> {
|
||||
const ns = getOperatorNamespace();
|
||||
const url = `${getBaseUrl()}/api/v1/namespaces/${ns}/configmaps/${OPENCLAW_CONFIGMAP_NAME}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { Accept: "application/json", ...getAuthHeaders() },
|
||||
});
|
||||
if (res.status === 404) {
|
||||
return { defaultTag: "" };
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err = new Error(
|
||||
`K8s GET configmap ${OPENCLAW_CONFIGMAP_NAME}: ${res.status} ${text}`
|
||||
);
|
||||
(err as any).statusCode = res.status;
|
||||
throw err;
|
||||
}
|
||||
const cm = (await res.json()) as { data?: Record<string, string> };
|
||||
return { defaultTag: cm.data?.defaultTag ?? "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the platform-default OpenClaw image tag. Empty string clears
|
||||
* the field (operator falls back to its built-in default).
|
||||
*
|
||||
* Creates the ConfigMap if it doesn't exist (PATCH on missing resource
|
||||
* 404s; we retry as POST). Keeps the admin UI usable on a fresh install
|
||||
* where the helm-shipped CM was deleted or never created.
|
||||
*/
|
||||
export async function setOpenClawDefaults(
|
||||
defaults: OpenClawDefaults
|
||||
): Promise<OpenClawDefaults> {
|
||||
const ns = getOperatorNamespace();
|
||||
const url = `${getBaseUrl()}/api/v1/namespaces/${ns}/configmaps/${OPENCLAW_CONFIGMAP_NAME}`;
|
||||
const patch = { data: { defaultTag: defaults.defaultTag } };
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/merge-patch+json",
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (res.status === 404) {
|
||||
const createUrl = `${getBaseUrl()}/api/v1/namespaces/${ns}/configmaps`;
|
||||
const createRes = await fetch(createUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "ConfigMap",
|
||||
metadata: { name: OPENCLAW_CONFIGMAP_NAME, namespace: ns },
|
||||
data: patch.data,
|
||||
}),
|
||||
});
|
||||
if (!createRes.ok) {
|
||||
const text = await createRes.text();
|
||||
throw new Error(
|
||||
`K8s POST configmap ${OPENCLAW_CONFIGMAP_NAME}: ${createRes.status} ${text}`
|
||||
);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(
|
||||
`K8s PATCH configmap ${OPENCLAW_CONFIGMAP_NAME}: ${res.status} ${text}`
|
||||
);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user