@oondemand/oon-core-back 0.6.11 → 0.6.12
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
CHANGED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Explicit prepared-member path. This module never imports the administrative
|
|
4
|
+
// provisioner, reads platform Secrets, or falls back to provisionRuntime.
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const { isDeepStrictEqual } = require("node:util");
|
|
8
|
+
const { resolveDeliveryTechnicalIdentity } = require("./technicalIdentity");
|
|
9
|
+
const MEMBERS = new Set(["meus-apps", "oon-workspace", "oon-docs"]);
|
|
10
|
+
const AUTHORITY_API = "https://central-ativacao.oonapps.online/api";
|
|
11
|
+
const ENVIRONMENTS = new Set(["desenvolvimento", "homologacao", "producao"]);
|
|
12
|
+
const OBJECT_ID = /^[a-f0-9]{24}$/;
|
|
13
|
+
const UID = /^[23456789abcdefghjkmnopqrstuvwxyz]{16}$/;
|
|
14
|
+
const VERSION = /^[0-9]+$/;
|
|
15
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
16
|
+
const PROFILES = Object.freeze({
|
|
17
|
+
nano: ["50m", "250m", "96Mi", "256Mi"], small: ["100m", "500m", "128Mi", "512Mi"],
|
|
18
|
+
medium: ["250m", "1", "256Mi", "1Gi"], large: ["500m", "2", "512Mi", "2Gi"],
|
|
19
|
+
});
|
|
20
|
+
function fail(code) { const e = new Error(code); e.code = code; throw e; }
|
|
21
|
+
function demand(value, code) { if (!value) fail(code); }
|
|
22
|
+
function stable(value) {
|
|
23
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
24
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])]));
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
const hash = value => crypto.createHash("sha256").update(JSON.stringify(stable(value))).digest("hex");
|
|
28
|
+
const copy = value => JSON.parse(JSON.stringify(value));
|
|
29
|
+
|
|
30
|
+
// authority is trusted executor configuration, separate from the member context.
|
|
31
|
+
// Callers must fetch/authorize the context through Central Prod; no browser input.
|
|
32
|
+
function expectedPreparation({ context: c, imageDigest, authority: a }) {
|
|
33
|
+
demand(c && a && MEMBERS.has(c.appCode) && c.repository === `oondemand/${c.appCode}` &&
|
|
34
|
+
c.lifecycleMode === "global_singleton" && ENVIRONMENTS.has(c.environment) &&
|
|
35
|
+
OBJECT_ID.test(c.appId || "") && OBJECT_ID.test(c.technicalInstanceId || "") &&
|
|
36
|
+
OBJECT_ID.test(c.deploymentId || "") && c.technicalIdentityVersion === 1 &&
|
|
37
|
+
/^[A-Za-z0-9_-]{1,120}$/.test(c.releaseId || "") && UUID.test(c.leaseId || "") &&
|
|
38
|
+
typeof c.deploymentToken === "string" && c.deploymentToken.length >= 32 && c.deploymentToken.length <= 8192 &&
|
|
39
|
+
/^g[a-z0-9-]{1,39}$/.test(a.installationId || "") && UID.test(a.instanceUid || "") &&
|
|
40
|
+
a.apiUrl === AUTHORITY_API && c.activationApiUrl === a.apiUrl && c.instanceUid !== a.instanceUid &&
|
|
41
|
+
/^sha256:[a-f0-9]{64}$/.test(imageDigest || "") && (!c.imageDigest || c.imageDigest === imageDigest) &&
|
|
42
|
+
c.imageRepository === `ghcr.io/oondemand/${c.appCode}`, "PREPARED_RUNTIME_CONTEXT_INVALID");
|
|
43
|
+
let id; try { id = resolveDeliveryTechnicalIdentity(c, { requireCanonical: true }); }
|
|
44
|
+
catch { fail("PREPARED_RUNTIME_IDENTITY_INVALID"); }
|
|
45
|
+
const frontendHostname = `${id.frontendDnsLabel}.oonapps.online`, backendHostname = `${id.backendDnsLabel}.oonapps.online`;
|
|
46
|
+
demand(c.namespace === id.namespace && c.frontendHostname === frontendHostname && c.backendHostname === backendHostname &&
|
|
47
|
+
c.frontendUrl === `https://${frontendHostname}` && c.backendUrl === `https://${backendHostname}/api` &&
|
|
48
|
+
c.publicUrl === c.frontendUrl && c.hostname === frontendHostname, "PREPARED_RUNTIME_URL_INVALID");
|
|
49
|
+
const profile = c.resourceProfile || "small";
|
|
50
|
+
demand(Object.hasOwn(PROFILES, profile), "PREPARED_RUNTIME_PROFILE_INVALID");
|
|
51
|
+
const binding = { schemaVersion: 1, installationId: a.installationId, authorityInstanceUid: a.instanceUid,
|
|
52
|
+
authorityApiUrl: a.apiUrl, appId: c.appId, appCode: c.appCode, technicalInstanceId: c.technicalInstanceId,
|
|
53
|
+
instanceUid: id.instanceUid, identityVersion: 1, environment: c.environment, namespace: id.namespace,
|
|
54
|
+
releaseId: c.releaseId, leaseId: c.leaseId, imageDigest, imageRepository: c.imageRepository,
|
|
55
|
+
deploymentId: c.deploymentId, deploymentTokenSha256: hash(c.deploymentToken),
|
|
56
|
+
frontendUrl: c.frontendUrl, backendUrl: c.backendUrl, resourceProfile: profile,
|
|
57
|
+
capabilitiesSha256: hash({ capabilities: c.capabilities || {}, functionalCapabilities: c.functionalCapabilities || {} }) };
|
|
58
|
+
return { binding, identity: id, receiptName: `oon-preparation-${hash(binding).slice(0, 32)}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pin(doc, name, namespace, kind) {
|
|
62
|
+
demand(doc?.kind === kind && doc.metadata?.name === name && doc.metadata.namespace === namespace &&
|
|
63
|
+
UUID.test(doc.metadata.uid || "") && VERSION.test(doc.metadata.resourceVersion || "") &&
|
|
64
|
+
!doc.metadata.deletionTimestamp, "PREPARED_RUNTIME_RESOURCE_INVALID");
|
|
65
|
+
return { name, uid: doc.metadata.uid, resourceVersion: doc.metadata.resourceVersion };
|
|
66
|
+
}
|
|
67
|
+
function samePin(doc, expected, namespace, kind) {
|
|
68
|
+
demand(isDeepStrictEqual(pin(doc, expected?.name, namespace, kind), expected), "PREPARED_RUNTIME_RESOURCE_CHANGED");
|
|
69
|
+
}
|
|
70
|
+
function decodeRuntime(secret) {
|
|
71
|
+
const values = {};
|
|
72
|
+
for (const [key, value] of Object.entries(secret.data || {})) {
|
|
73
|
+
demand(typeof value === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value), "PREPARED_RUNTIME_SECRET_INVALID");
|
|
74
|
+
values[key] = Buffer.from(value, "base64").toString("utf8");
|
|
75
|
+
}
|
|
76
|
+
return values;
|
|
77
|
+
}
|
|
78
|
+
function assertRuntime(secret, c, id, a) {
|
|
79
|
+
const env = decodeRuntime(secret);
|
|
80
|
+
const expected = { NODE_ENV: "production", APP_ENVIRONMENT: c.environment, APP_CODE: c.appCode,
|
|
81
|
+
PUBLIC_APP_URL: c.frontendUrl, CENTRAL_ATIVACAO_API_URL: a.apiUrl,
|
|
82
|
+
CENTRAL_ATIVACAO_URL: a.apiUrl.replace(/\/api$/, ""), OON_INSTANCE_UID: id.instanceUid,
|
|
83
|
+
OON_TECHNICAL_IDENTITY_VERSION: "1", OON_DEPLOYMENT_ID: c.deploymentId,
|
|
84
|
+
OON_DEPLOYMENT_TOKEN: c.deploymentToken, OON_APP_CODE: c.appCode };
|
|
85
|
+
demand(Object.entries(expected).every(([k, v]) => env[k] === v) &&
|
|
86
|
+
typeof env.INSTANCE_CREDENTIAL_ENCRYPTION_KEY === "string" && env.INSTANCE_CREDENTIAL_ENCRYPTION_KEY.length >= 32 &&
|
|
87
|
+
!["MONGO_ADMIN_URI", "DIGITALOCEAN_TOKEN", "KUBECONFIG", "KUBECONFIG_B64", "PUBLISHER_KUBECONFIG_B64",
|
|
88
|
+
"GITHUB_APP_PRIVATE_KEY", "GITHUB_CONTROL_PLANE_APP_PRIVATE_KEY"].some(k => Object.hasOwn(env, k)), "PREPARED_RUNTIME_SECRET_INVALID");
|
|
89
|
+
let uri, username; try { uri = new URL(env.MONGO_URI); username = decodeURIComponent(uri.username); }
|
|
90
|
+
catch { fail("PREPARED_RUNTIME_MONGO_INVALID"); }
|
|
91
|
+
demand(["mongodb:", "mongodb+srv:"].includes(uri.protocol) && uri.hostname && uri.password &&
|
|
92
|
+
username === id.mongoUsername && uri.pathname === `/${id.database}` &&
|
|
93
|
+
uri.searchParams.get("authSource") === "admin", "PREPARED_RUNTIME_MONGO_INVALID");
|
|
94
|
+
if (c.functionalCapabilities?.["core.transactional-email"]?.enabled) {
|
|
95
|
+
let ring; try { ring = JSON.parse(env.OON_TRANSACTIONAL_EMAIL_KEK_KEYRING); } catch { fail("PREPARED_RUNTIME_CAPABILITY_MISSING"); }
|
|
96
|
+
demand(typeof ring.currentKeyId === "string" && typeof ring.keys?.[ring.currentKeyId] === "string" &&
|
|
97
|
+
Buffer.from(ring.keys[ring.currentKeyId], "base64").length === 32, "PREPARED_RUNTIME_CAPABILITY_MISSING");
|
|
98
|
+
}
|
|
99
|
+
if (c.capabilities?.pdfRendering) {
|
|
100
|
+
demand(env.OON_CAPABILITY_PDF_RENDERING_URL === c.capabilities.pdfRendering.endpoint &&
|
|
101
|
+
env.OON_CAPABILITY_PDF_RENDERING_VERSION === c.capabilities.pdfRendering.implementationVersion &&
|
|
102
|
+
env.OON_CAPABILITY_PDF_RENDERING_USERNAME && env.OON_CAPABILITY_PDF_RENDERING_PASSWORD, "PREPARED_RUNTIME_CAPABILITY_MISSING");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function kubernetesClient(spawn = spawnSync) {
|
|
107
|
+
const command = (args, input, code) => {
|
|
108
|
+
let r; try { r = spawn("kubectl", [...args, "--request-timeout=60s"], {
|
|
109
|
+
input: input === undefined ? undefined : JSON.stringify(input), encoding: "utf8", timeout: 70000,
|
|
110
|
+
maxBuffer: 4 * 1024 * 1024, stdio: ["pipe", "pipe", "pipe"],
|
|
111
|
+
}); } catch { fail(code); }
|
|
112
|
+
if (r.error || r.status !== 0) fail(code); // Never include provider output or args in diagnostics.
|
|
113
|
+
return String(r.stdout || "");
|
|
114
|
+
};
|
|
115
|
+
const json = (raw, code) => { try { return JSON.parse(raw); } catch { fail(code); } };
|
|
116
|
+
return {
|
|
117
|
+
read: (kind, name, namespace) => {
|
|
118
|
+
const raw = command(["-n", namespace, "get", kind, name, "--ignore-not-found", "-o", "json"], undefined, "PREPARED_RUNTIME_READ_FAILED");
|
|
119
|
+
return raw.trim() ? json(raw, "PREPARED_RUNTIME_READ_FAILED") : null;
|
|
120
|
+
},
|
|
121
|
+
write: (doc, existing) => {
|
|
122
|
+
let next = copy(doc);
|
|
123
|
+
if (existing) {
|
|
124
|
+
next.metadata = { ...existing.metadata, ...next.metadata, resourceVersion: existing.metadata.resourceVersion,
|
|
125
|
+
uid: existing.metadata.uid, labels: { ...existing.metadata.labels, ...next.metadata.labels },
|
|
126
|
+
annotations: { ...next.metadata.annotations } };
|
|
127
|
+
delete next.metadata.managedFields;
|
|
128
|
+
if (next.kind === "Service") {
|
|
129
|
+
for (const field of ["clusterIP", "clusterIPs", "ipFamilies", "ipFamilyPolicy", "internalTrafficPolicy"]) {
|
|
130
|
+
if (existing.spec[field] !== undefined) next.spec[field] = existing.spec[field];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// create refuses a concurrent occupant; replace uses resourceVersion CAS.
|
|
135
|
+
return json(command([existing ? "replace" : "create", "-f", "-", "-o", "json"], next,
|
|
136
|
+
"PREPARED_RUNTIME_WRITE_OUTCOME_UNKNOWN"), "PREPARED_RUNTIME_WRITE_OUTCOME_UNKNOWN");
|
|
137
|
+
},
|
|
138
|
+
rollout: (name, namespace) => command(["-n", namespace, "rollout", "status", `deployment/${name}`, "--timeout=60s"], undefined, "PREPARED_RUNTIME_ROLLOUT_PENDING"),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderPreparedRuntime({ context: c, imageDigest, authority }, receipt) {
|
|
143
|
+
const { identity: id, binding } = expectedPreparation({ context: c, imageDigest, authority });
|
|
144
|
+
const [cpuRequest, cpuLimit, memoryRequest, memoryLimit] = PROFILES[binding.resourceProfile];
|
|
145
|
+
const labels = { "app.kubernetes.io/name": c.appCode, "oon.runtime/instance-uid": id.instanceUid, "oon.runtime/identity-version": "1" };
|
|
146
|
+
const ownership = { "oon.delivery/installation": authority.installationId, "oon.delivery/authority-uid": authority.instanceUid,
|
|
147
|
+
"oon.delivery/technical-instance": c.technicalInstanceId, "oon.delivery/environment": c.environment };
|
|
148
|
+
const metadata = name => ({ name, namespace: id.namespace, labels, annotations: ownership });
|
|
149
|
+
const ingress = (name, hostname, tlsSecret, route) => ({ apiVersion: "networking.k8s.io/v1", kind: "Ingress",
|
|
150
|
+
metadata: { ...metadata(name), annotations: { ...ownership, "cert-manager.io/cluster-issuer": receipt.clusterIssuer } },
|
|
151
|
+
spec: { ingressClassName: receipt.ingressClass, tls: [{ hosts: [hostname], secretName: tlsSecret }],
|
|
152
|
+
rules: [{ host: hostname, http: { paths: [{ path: route, pathType: "Prefix", backend: { service: { name: c.appCode, port: { name: "http" } } } }] } }] } });
|
|
153
|
+
return [
|
|
154
|
+
{ apiVersion: "apps/v1", kind: "Deployment", metadata: metadata(c.appCode), spec: {
|
|
155
|
+
replicas: c.environment === "producao" ? 2 : 1, revisionHistoryLimit: 2,
|
|
156
|
+
strategy: { type: "RollingUpdate", rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } },
|
|
157
|
+
selector: { matchLabels: { "app.kubernetes.io/name": c.appCode } },
|
|
158
|
+
template: { metadata: { labels, annotations: { ...ownership, "oon.delivery/release": c.releaseId,
|
|
159
|
+
"oon.delivery/lease": c.leaseId, "oon.delivery/image-digest": imageDigest } }, spec: {
|
|
160
|
+
serviceAccountName: id.serviceAccount, automountServiceAccountToken: false,
|
|
161
|
+
securityContext: { runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000, seccompProfile: { type: "RuntimeDefault" } },
|
|
162
|
+
imagePullSecrets: [{ name: receipt.pullSecret.name }], containers: [{ name: "central", image: `${c.imageRepository}@${imageDigest}`,
|
|
163
|
+
imagePullPolicy: "IfNotPresent", ports: [{ name: "http", containerPort: 8080 }], envFrom: [{ secretRef: { name: id.secret } }],
|
|
164
|
+
securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } },
|
|
165
|
+
resources: { requests: { cpu: cpuRequest, memory: memoryRequest }, limits: { cpu: cpuLimit, memory: memoryLimit } },
|
|
166
|
+
livenessProbe: { httpGet: { path: "/health/live", port: "http" }, initialDelaySeconds: 20 },
|
|
167
|
+
readinessProbe: { httpGet: { path: "/api/health/ready", port: "http" }, initialDelaySeconds: 10 } }],
|
|
168
|
+
} },
|
|
169
|
+
} },
|
|
170
|
+
{ apiVersion: "v1", kind: "Service", metadata: metadata(c.appCode), spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": c.appCode }, ports: [{ name: "http", port: 80, targetPort: "http", protocol: "TCP" }] } },
|
|
171
|
+
ingress(c.appCode, c.frontendHostname, id.tlsSecret, "/"),
|
|
172
|
+
ingress(`${c.appCode}-api`, c.backendHostname, `${c.appCode}-api-tls`, "/api"),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function deployPreparedRuntime(options, dependencies = {}) {
|
|
177
|
+
const frozen = copy(options); // No caller mutation while awaiting I/O.
|
|
178
|
+
const { context: c, authority: a } = frozen;
|
|
179
|
+
const { binding, identity: id, receiptName } = expectedPreparation(frozen);
|
|
180
|
+
const kube = dependencies.kube || kubernetesClient();
|
|
181
|
+
const receiptDoc = await kube.read("configmap", receiptName, id.namespace);
|
|
182
|
+
demand(receiptDoc, "PREPARED_RUNTIME_REQUIRED");
|
|
183
|
+
const receiptPin = pin(receiptDoc, receiptName, id.namespace, "ConfigMap");
|
|
184
|
+
let receipt; try { receipt = JSON.parse(receiptDoc.data?.receipt); } catch { fail("PREPARED_RUNTIME_RECEIPT_INVALID"); }
|
|
185
|
+
demand(receiptDoc.immutable === true && receipt?.phase === "prepared" && isDeepStrictEqual(receipt.binding, binding) &&
|
|
186
|
+
receipt.ingressClass === "nginx" && receipt.clusterIssuer === "letsencrypt-production-http01" &&
|
|
187
|
+
receipt.runtimeSecret?.name === id.secret && receipt.runtimeServiceAccount?.name === id.serviceAccount &&
|
|
188
|
+
receipt.pullSecret?.name === "ghcr-oon", "PREPARED_RUNTIME_RECEIPT_INVALID");
|
|
189
|
+
const checkPreparation = async () => {
|
|
190
|
+
samePin(await kube.read("configmap", receiptName, id.namespace), receiptPin, id.namespace, "ConfigMap");
|
|
191
|
+
const secret = await kube.read("secret", id.secret, id.namespace);
|
|
192
|
+
samePin(secret, receipt.runtimeSecret, id.namespace, "Secret");
|
|
193
|
+
assertRuntime(secret, c, id, a);
|
|
194
|
+
const sa = await kube.read("serviceaccount", id.serviceAccount, id.namespace);
|
|
195
|
+
samePin(sa, receipt.runtimeServiceAccount, id.namespace, "ServiceAccount");
|
|
196
|
+
demand(sa.automountServiceAccountToken === false, "PREPARED_RUNTIME_RESOURCE_INVALID");
|
|
197
|
+
const pull = await kube.read("secret", receipt.pullSecret.name, id.namespace);
|
|
198
|
+
samePin(pull, receipt.pullSecret, id.namespace, "Secret");
|
|
199
|
+
demand(pull.type === "kubernetes.io/dockerconfigjson", "PREPARED_RUNTIME_RESOURCE_INVALID");
|
|
200
|
+
};
|
|
201
|
+
await checkPreparation();
|
|
202
|
+
const docs = renderPreparedRuntime(frozen, receipt), existing = [];
|
|
203
|
+
for (const doc of docs) {
|
|
204
|
+
const current = await kube.read(doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
|
|
205
|
+
if (current) {
|
|
206
|
+
pin(current, doc.metadata.name, id.namespace, doc.kind);
|
|
207
|
+
const ownership = doc.metadata.annotations;
|
|
208
|
+
demand(Object.entries(ownership).every(([k,v]) => current.metadata.annotations?.[k] === v), "PREPARED_RUNTIME_TARGET_OCCUPIED");
|
|
209
|
+
// An owned Ingress may still carry routing/auth/snippet directives that
|
|
210
|
+
// change its behavior. Require explicit reconciliation before any write.
|
|
211
|
+
demand(isDeepStrictEqual(current.metadata.annotations, ownership), "PREPARED_RUNTIME_ANNOTATIONS_UNAPPROVED");
|
|
212
|
+
}
|
|
213
|
+
existing.push(current);
|
|
214
|
+
}
|
|
215
|
+
for (let index = 0; index < docs.length; index++) {
|
|
216
|
+
await checkPreparation();
|
|
217
|
+
const doc = docs[index], before = existing[index], current = await kube.read(doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
|
|
218
|
+
demand(before ? current?.metadata?.uid === before.metadata.uid && current.metadata.resourceVersion === before.metadata.resourceVersion : current === null,
|
|
219
|
+
"PREPARED_RUNTIME_TARGET_CHANGED");
|
|
220
|
+
await kube.write(doc, before);
|
|
221
|
+
}
|
|
222
|
+
await checkPreparation();
|
|
223
|
+
await kube.rollout(c.appCode, id.namespace);
|
|
224
|
+
await checkPreparation();
|
|
225
|
+
return { infraVersion: "prepared-member-v1", imageRef: `${c.imageRepository}@${frozen.imageDigest}`,
|
|
226
|
+
technicalIdentity: id, preparation: { name: receiptName, uid: receiptPin.uid },
|
|
227
|
+
proofLimits: ["no-distributed-lock-with-administrative-preparation", "rollout-is-not-functional-acceptance"] };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = { expectedPreparation, renderPreparedRuntime, deployPreparedRuntime, kubernetesClient };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Runtime preparado para membros da plataforma — B5
|
|
2
|
+
|
|
3
|
+
API opt-in `deployPreparedRuntime({context, imageDigest, authority})`, exportada por
|
|
4
|
+
`delivery/runtime.js`, para `meus-apps`, `oon-workspace` e `oon-docs`. O executor atual
|
|
5
|
+
carrega delivery pelo caminho físico do pacote; nenhum novo subpath npm é exportado.
|
|
6
|
+
O workflow `version-pr` prepara a versão 0.6.12; integrar em main aciona
|
|
7
|
+
`release-npm`, que publica os três pacotes após validação. A publicação só deve ser
|
|
8
|
+
registrada como concluída após o run de release. Isso não liga os consumidores
|
|
9
|
+
existentes ao novo adapter.
|
|
10
|
+
|
|
11
|
+
## Fronteira de confiança
|
|
12
|
+
|
|
13
|
+
`context` deve vir da Central Prod pelo protocolo autenticado de publicação e lease.
|
|
14
|
+
`authority` é configuração confiável separada, com `installationId`, `instanceUid`
|
|
15
|
+
da raiz e `apiUrl`. Nenhum desses valores pode vir de input livre do workflow.
|
|
16
|
+
O adapter valida consistência; não consulta Mongo, autentica Central nem autoriza
|
|
17
|
+
uma reserva. O consumidor deve validar lease vigente e comando antes de chamá-lo.
|
|
18
|
+
|
|
19
|
+
A preparação administrativa ainda será implementada na Infra. Deve ler novamente
|
|
20
|
+
singleton/configuração bloqueada, comandos e concorrência; conferir identidade,
|
|
21
|
+
release Prod e reservas nativas; preparar somente os alvos aprovados. Durante a
|
|
22
|
+
publicação, deve manter a exclusão mútua e a vigência do lease com a Central. Não
|
|
23
|
+
usar `ensureTechnicalInstance` para descoberta, nem adotar recursos por labels.
|
|
24
|
+
|
|
25
|
+
O preparador administra Namespace, quota, LimitRange, políticas de rede/admissão,
|
|
26
|
+
ServiceAccount runtime sem token, usuário Mongo limitado ao banco, runtime Secret,
|
|
27
|
+
pull secret e capacidades. Deve preservar recursos/chaves existentes e parar em
|
|
28
|
+
divergência. Nenhuma credencial administrativa deve ser projetada no membro.
|
|
29
|
+
|
|
30
|
+
## Recibo por publicação
|
|
31
|
+
|
|
32
|
+
`expectedPreparation(options)` retorna `binding`, `identity` e `receiptName`.
|
|
33
|
+
O preparador deverá usar a mesma função/versão fixada. `receiptName` é
|
|
34
|
+
`oon-preparation-` mais os primeiros 32 hex de SHA256 do JSON canônico de `binding`.
|
|
35
|
+
Objetos são ordenados recursivamente; arrays mantêm ordem. O hash do token também
|
|
36
|
+
usa JSON canônico da string, não os bytes crus. Nunca persistir o token no recibo.
|
|
37
|
+
|
|
38
|
+
No namespace canônico, criar ConfigMap `immutable: true` com `data.receipt` contendo
|
|
39
|
+
JSON com estes campos:
|
|
40
|
+
|
|
41
|
+
| Campo | Valor exigido |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `phase` | `prepared` |
|
|
44
|
+
| `binding` | Exatamente o retornado pelo SDK: autoridade, identidade, ambiente, release, lease, imagem/digest, deployment, URLs, perfil e hashes das capacidades/token |
|
|
45
|
+
| `ingressClass` | `nginx` |
|
|
46
|
+
| `clusterIssuer` | `letsencrypt-production-http01` |
|
|
47
|
+
| `runtimeSecret` | `{name, uid, resourceVersion}` do Secret canônico final |
|
|
48
|
+
| `runtimeServiceAccount` | `{name, uid, resourceVersion}` da SA canônica final |
|
|
49
|
+
| `pullSecret` | `{name: "ghcr-oon", uid, resourceVersion}` do Secret local final |
|
|
50
|
+
|
|
51
|
+
O recibo é o último recurso da preparação. Não é uma assinatura: sua autoridade
|
|
52
|
+
depende de o executor não poder criar/alterar/apagar ConfigMaps, Secrets ou SAs.
|
|
53
|
+
Não reutilizar recibo em outro lease/digest. UID e resourceVersion dos recursos são
|
|
54
|
+
reconferidos antes de cada escrita e após o rollout. Isso detecta deriva observada,
|
|
55
|
+
mas não substitui um lock distribuído com o preparador.
|
|
56
|
+
|
|
57
|
+
O runtime Secret deve conter identidade/URLs, credencial de deployment e chave de
|
|
58
|
+
criptografia já preparadas. Mongo exige URI de host único `mongodb`/`mongodb+srv`,
|
|
59
|
+
usuário e banco canônicos e `authSource=admin`; o adapter não prova os privilégios
|
|
60
|
+
desse usuário. E-mail habilitado exige keyring existente; PDF exige endpoint,
|
|
61
|
+
versão e credenciais projetadas. O adapter nunca gera chaves ou busca Secrets
|
|
62
|
+
administrativos. A recusa de nomes administrativos conhecidos é defesa adicional,
|
|
63
|
+
não uma prova da origem ou do escopo de todas as credenciais.
|
|
64
|
+
|
|
65
|
+
## Permissões necessárias e limites
|
|
66
|
+
|
|
67
|
+
Leitura `get` somente dos nomes do recibo, runtime Secret, pull Secret e runtime SA
|
|
68
|
+
no alvo; leitura/escrita de Deployment, Service e dois Ingresses canônicos; rollout
|
|
69
|
+
consulta/observa o Deployment nomeado. Não requer namespaces, RBAC, nodes, jobs,
|
|
70
|
+
pods/exec, serviceaccounts/token ou acesso a `oon-system`/outros namespaces.
|
|
71
|
+
|
|
72
|
+
Não aplicar uma Role genérica com base apenas nesta lista. `create` de recursos
|
|
73
|
+
principais não pode ser limitado por `resourceNames` no RBAC; workloads também
|
|
74
|
+
podem montar Secrets e escolher SAs. Antes de conceder escrita, a Infra deve
|
|
75
|
+
implementar e provar admissão restritiva (nomes, imagem, hosts, SAs, referências a
|
|
76
|
+
Secrets, pod security) ou um mecanismo administrativo equivalente. RBAC deve ser
|
|
77
|
+
por alvo/ambiente/instalação, com autenticação dedicada e recusas cruzadas. Ver
|
|
78
|
+
[limites oficiais do RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources).
|
|
79
|
+
|
|
80
|
+
O renderer produz somente Deployment, Service e Ingresses frontend/backend, por
|
|
81
|
+
digest, sem token de SA, sem privilégio, UID/GID 1000 e seccomp RuntimeDefault.
|
|
82
|
+
A compatibilidade das imagens dos três membros com esse usuário ainda precisa
|
|
83
|
+
ser provada. Não oferece manifest arbitrário, initContainers, volumes ou hooks.
|
|
84
|
+
|
|
85
|
+
Um alvo existente exige ownership correspondente e UID/resourceVersion observados;
|
|
86
|
+
annotations adicionais são recusadas antes de qualquer escrita, inclusive regras
|
|
87
|
+
de rewrite, redirect, autenticação ou snippets de Ingress. Exigem reconciliação
|
|
88
|
+
administrativa explícita; não são herdadas silenciosamente na substituição.
|
|
89
|
+
`replace` usa CAS, `create` recusa ocupante concorrente. Essa conferência no cliente
|
|
90
|
+
não é autorização nem prova de ownership canônico no banco. Não adota recursos
|
|
91
|
+
legados. Falha parcial/resultado de escrita incerto interrompe sem retry ou rollback.
|
|
92
|
+
O operador deve reconciliar resultado com o comando antes de tentar novamente.
|
|
93
|
+
Rollout tem limite de 60 segundos; timeout é pendência, não motivo para nova escrita.
|
|
94
|
+
Rollout bem-sucedido não prova HTTP, TLS, callback ou aceite funcional.
|
|
95
|
+
|
|
96
|
+
## Gate de uso
|
|
97
|
+
|
|
98
|
+
Manter delivery bloqueado até implementar preparador e admissão/bindings, provar
|
|
99
|
+
positivos e recusas por alvo, publicar a versão do SDK e ligar ambos os consumidores
|
|
100
|
+
ao novo adapter. O preparador de kubeconfig atual exige `create namespaces` e o
|
|
101
|
+
script atual ainda chama provisionamento administrativo: ambos precisam de caminho
|
|
102
|
+
específico para membros. Não satisfazer esse guard concedendo namespace create.
|
|
103
|
+
Nenhuma chamada deve cair no provisionador antigo se o recibo estiver ausente.
|
|
104
|
+
|
|
105
|
+
Testes locais: `node --test packages/oonCore-back/test/prepared-runtime.test.js
|
|
106
|
+
packages/oonCore-back/test/delivery-technical-identity.test.js`. Os fakes verificam
|
|
107
|
+
comportamento do adapter; não comprovam permissões Kubernetes ou integração real.
|
package/src/delivery/runtime.js
CHANGED
|
@@ -363,6 +363,8 @@ module.exports = {
|
|
|
363
363
|
runtimeManifest,
|
|
364
364
|
provisionRuntime,
|
|
365
365
|
deployRuntime,
|
|
366
|
+
// Explicit opt-in for a separately prepared target; never a provisioning fallback.
|
|
367
|
+
deployPreparedRuntime: require("./preparedRuntime").deployPreparedRuntime,
|
|
366
368
|
waitHttp,
|
|
367
369
|
activateMember,
|
|
368
370
|
};
|