@oondemand/oon-core-back 0.6.12 → 0.6.13
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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// provisioner, reads platform Secrets, or falls back to provisionRuntime.
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
6
|
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const { performance } = require("node:perf_hooks");
|
|
7
8
|
const { isDeepStrictEqual } = require("node:util");
|
|
8
9
|
const { resolveDeliveryTechnicalIdentity } = require("./technicalIdentity");
|
|
9
10
|
const MEMBERS = new Set(["meus-apps", "oon-workspace", "oon-docs"]);
|
|
@@ -27,6 +28,30 @@ function stable(value) {
|
|
|
27
28
|
const hash = value => crypto.createHash("sha256").update(JSON.stringify(stable(value))).digest("hex");
|
|
28
29
|
const copy = value => JSON.parse(JSON.stringify(value));
|
|
29
30
|
|
|
31
|
+
function leaseDeadline(value) {
|
|
32
|
+
const deadline = typeof value === "string" ? Date.parse(value) : NaN;
|
|
33
|
+
demand(Number.isFinite(deadline) && new Date(deadline).toISOString() === value,
|
|
34
|
+
"PREPARED_RUNTIME_LEASE_INVALID");
|
|
35
|
+
return deadline;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Local freshness only. An in-flight API request may already have committed;
|
|
39
|
+
// expiry never authorizes takeover, rollback or release of an administrative lock.
|
|
40
|
+
function leaseGuard(expiresAt, { now = Date.now, monotonicNow = () => performance.now() } = {}) {
|
|
41
|
+
const deadline = leaseDeadline(expiresAt), started = now(), monotonicStarted = monotonicNow();
|
|
42
|
+
demand(Number.isFinite(started) && Number.isFinite(monotonicStarted), "PREPARED_RUNTIME_CLOCK_INVALID");
|
|
43
|
+
let lastMonotonic = monotonicStarted;
|
|
44
|
+
return () => {
|
|
45
|
+
const current = now(), monotonic = monotonicNow();
|
|
46
|
+
demand(Number.isFinite(current) && Number.isFinite(monotonic) && monotonic >= lastMonotonic,
|
|
47
|
+
"PREPARED_RUNTIME_CLOCK_INVALID");
|
|
48
|
+
lastMonotonic = monotonic;
|
|
49
|
+
// The original remaining budget cannot grow if the wall clock moves back.
|
|
50
|
+
demand(current < deadline && monotonic - monotonicStarted < deadline - started,
|
|
51
|
+
"PREPARED_RUNTIME_LEASE_EXPIRED");
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
// authority is trusted executor configuration, separate from the member context.
|
|
31
56
|
// Callers must fetch/authorize the context through Central Prod; no browser input.
|
|
32
57
|
function expectedPreparation({ context: c, imageDigest, authority: a }) {
|
|
@@ -40,6 +65,7 @@ function expectedPreparation({ context: c, imageDigest, authority: a }) {
|
|
|
40
65
|
a.apiUrl === AUTHORITY_API && c.activationApiUrl === a.apiUrl && c.instanceUid !== a.instanceUid &&
|
|
41
66
|
/^sha256:[a-f0-9]{64}$/.test(imageDigest || "") && (!c.imageDigest || c.imageDigest === imageDigest) &&
|
|
42
67
|
c.imageRepository === `ghcr.io/oondemand/${c.appCode}`, "PREPARED_RUNTIME_CONTEXT_INVALID");
|
|
68
|
+
leaseDeadline(c.leaseExpiresAt);
|
|
43
69
|
let id; try { id = resolveDeliveryTechnicalIdentity(c, { requireCanonical: true }); }
|
|
44
70
|
catch { fail("PREPARED_RUNTIME_IDENTITY_INVALID"); }
|
|
45
71
|
const frontendHostname = `${id.frontendDnsLabel}.oonapps.online`, backendHostname = `${id.backendDnsLabel}.oonapps.online`;
|
|
@@ -48,10 +74,10 @@ function expectedPreparation({ context: c, imageDigest, authority: a }) {
|
|
|
48
74
|
c.publicUrl === c.frontendUrl && c.hostname === frontendHostname, "PREPARED_RUNTIME_URL_INVALID");
|
|
49
75
|
const profile = c.resourceProfile || "small";
|
|
50
76
|
demand(Object.hasOwn(PROFILES, profile), "PREPARED_RUNTIME_PROFILE_INVALID");
|
|
51
|
-
const binding = { schemaVersion:
|
|
77
|
+
const binding = { schemaVersion: 2, installationId: a.installationId, authorityInstanceUid: a.instanceUid,
|
|
52
78
|
authorityApiUrl: a.apiUrl, appId: c.appId, appCode: c.appCode, technicalInstanceId: c.technicalInstanceId,
|
|
53
79
|
instanceUid: id.instanceUid, identityVersion: 1, environment: c.environment, namespace: id.namespace,
|
|
54
|
-
releaseId: c.releaseId, leaseId: c.leaseId, imageDigest, imageRepository: c.imageRepository,
|
|
80
|
+
releaseId: c.releaseId, leaseId: c.leaseId, leaseExpiresAt: c.leaseExpiresAt, imageDigest, imageRepository: c.imageRepository,
|
|
55
81
|
deploymentId: c.deploymentId, deploymentTokenSha256: hash(c.deploymentToken),
|
|
56
82
|
frontendUrl: c.frontendUrl, backendUrl: c.backendUrl, resourceProfile: profile,
|
|
57
83
|
capabilitiesSha256: hash({ capabilities: c.capabilities || {}, functionalCapabilities: c.functionalCapabilities || {} }) };
|
|
@@ -178,7 +204,14 @@ async function deployPreparedRuntime(options, dependencies = {}) {
|
|
|
178
204
|
const { context: c, authority: a } = frozen;
|
|
179
205
|
const { binding, identity: id, receiptName } = expectedPreparation(frozen);
|
|
180
206
|
const kube = dependencies.kube || kubernetesClient();
|
|
181
|
-
const
|
|
207
|
+
const checkLease = leaseGuard(c.leaseExpiresAt, dependencies);
|
|
208
|
+
const io = async (operation, ...args) => {
|
|
209
|
+
checkLease();
|
|
210
|
+
const result = await kube[operation](...args);
|
|
211
|
+
checkLease();
|
|
212
|
+
return result;
|
|
213
|
+
};
|
|
214
|
+
const receiptDoc = await io("read", "configmap", receiptName, id.namespace);
|
|
182
215
|
demand(receiptDoc, "PREPARED_RUNTIME_REQUIRED");
|
|
183
216
|
const receiptPin = pin(receiptDoc, receiptName, id.namespace, "ConfigMap");
|
|
184
217
|
let receipt; try { receipt = JSON.parse(receiptDoc.data?.receipt); } catch { fail("PREPARED_RUNTIME_RECEIPT_INVALID"); }
|
|
@@ -187,21 +220,21 @@ async function deployPreparedRuntime(options, dependencies = {}) {
|
|
|
187
220
|
receipt.runtimeSecret?.name === id.secret && receipt.runtimeServiceAccount?.name === id.serviceAccount &&
|
|
188
221
|
receipt.pullSecret?.name === "ghcr-oon", "PREPARED_RUNTIME_RECEIPT_INVALID");
|
|
189
222
|
const checkPreparation = async () => {
|
|
190
|
-
samePin(await
|
|
191
|
-
const secret = await
|
|
223
|
+
samePin(await io("read", "configmap", receiptName, id.namespace), receiptPin, id.namespace, "ConfigMap");
|
|
224
|
+
const secret = await io("read", "secret", id.secret, id.namespace);
|
|
192
225
|
samePin(secret, receipt.runtimeSecret, id.namespace, "Secret");
|
|
193
226
|
assertRuntime(secret, c, id, a);
|
|
194
|
-
const sa = await
|
|
227
|
+
const sa = await io("read", "serviceaccount", id.serviceAccount, id.namespace);
|
|
195
228
|
samePin(sa, receipt.runtimeServiceAccount, id.namespace, "ServiceAccount");
|
|
196
229
|
demand(sa.automountServiceAccountToken === false, "PREPARED_RUNTIME_RESOURCE_INVALID");
|
|
197
|
-
const pull = await
|
|
230
|
+
const pull = await io("read", "secret", receipt.pullSecret.name, id.namespace);
|
|
198
231
|
samePin(pull, receipt.pullSecret, id.namespace, "Secret");
|
|
199
232
|
demand(pull.type === "kubernetes.io/dockerconfigjson", "PREPARED_RUNTIME_RESOURCE_INVALID");
|
|
200
233
|
};
|
|
201
234
|
await checkPreparation();
|
|
202
235
|
const docs = renderPreparedRuntime(frozen, receipt), existing = [];
|
|
203
236
|
for (const doc of docs) {
|
|
204
|
-
const current = await
|
|
237
|
+
const current = await io("read", doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
|
|
205
238
|
if (current) {
|
|
206
239
|
pin(current, doc.metadata.name, id.namespace, doc.kind);
|
|
207
240
|
const ownership = doc.metadata.annotations;
|
|
@@ -214,15 +247,16 @@ async function deployPreparedRuntime(options, dependencies = {}) {
|
|
|
214
247
|
}
|
|
215
248
|
for (let index = 0; index < docs.length; index++) {
|
|
216
249
|
await checkPreparation();
|
|
217
|
-
const doc = docs[index], before = existing[index], current = await
|
|
250
|
+
const doc = docs[index], before = existing[index], current = await io("read", doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
|
|
218
251
|
demand(before ? current?.metadata?.uid === before.metadata.uid && current.metadata.resourceVersion === before.metadata.resourceVersion : current === null,
|
|
219
252
|
"PREPARED_RUNTIME_TARGET_CHANGED");
|
|
220
|
-
await
|
|
253
|
+
await io("write", doc, before);
|
|
221
254
|
}
|
|
222
255
|
await checkPreparation();
|
|
223
|
-
await
|
|
256
|
+
await io("rollout", c.appCode, id.namespace);
|
|
224
257
|
await checkPreparation();
|
|
225
|
-
|
|
258
|
+
checkLease();
|
|
259
|
+
return { infraVersion: "prepared-member-v2", imageRef: `${c.imageRepository}@${frozen.imageDigest}`,
|
|
226
260
|
technicalIdentity: id, preparation: { name: receiptName, uid: receiptPin.uid },
|
|
227
261
|
proofLimits: ["no-distributed-lock-with-administrative-preparation", "rollout-is-not-functional-acceptance"] };
|
|
228
262
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
API opt-in `deployPreparedRuntime({context, imageDigest, authority})`, exportada por
|
|
4
4
|
`delivery/runtime.js`, para `meus-apps`, `oon-workspace` e `oon-docs`. O executor atual
|
|
5
5
|
carrega delivery pelo caminho físico do pacote; nenhum novo subpath npm é exportado.
|
|
6
|
-
O workflow `version-pr` prepara a versão
|
|
6
|
+
O workflow `version-pr` prepara a próxima versão patch; integrar em main aciona
|
|
7
7
|
`release-npm`, que publica os três pacotes após validação. A publicação só deve ser
|
|
8
8
|
registrada como concluída após o run de release. Isso não liga os consumidores
|
|
9
9
|
existentes ao novo adapter.
|
|
@@ -15,6 +15,10 @@ existentes ao novo adapter.
|
|
|
15
15
|
da raiz e `apiUrl`. Nenhum desses valores pode vir de input livre do workflow.
|
|
16
16
|
O adapter valida consistência; não consulta Mongo, autentica Central nem autoriza
|
|
17
17
|
uma reserva. O consumidor deve validar lease vigente e comando antes de chamá-lo.
|
|
18
|
+
O contexto autenticado precisa incluir `leaseExpiresAt` no formato ISO UTC canônico
|
|
19
|
+
retornado pela Central (`Date.toISOString()`). Ausência, formato inválido ou prazo
|
|
20
|
+
vencido interrompem antes de acessar Kubernetes. Não completar esse campo no
|
|
21
|
+
consumidor com um prazo local nem renovar o lease para reutilizar um recibo.
|
|
18
22
|
|
|
19
23
|
A preparação administrativa ainda será implementada na Infra. Deve ler novamente
|
|
20
24
|
singleton/configuração bloqueada, comandos e concorrência; conferir identidade,
|
|
@@ -30,6 +34,13 @@ divergência. Nenhuma credencial administrativa deve ser projetada no membro.
|
|
|
30
34
|
## Recibo por publicação
|
|
31
35
|
|
|
32
36
|
`expectedPreparation(options)` retorna `binding`, `identity` e `receiptName`.
|
|
37
|
+
O binding agora usa `schemaVersion: 2` e inclui `leaseExpiresAt` além de `leaseId`.
|
|
38
|
+
O cálculo é puro e valida o formato, não a vigência no instante de uso; o preparador
|
|
39
|
+
também precisa revalidar a autoridade e o prazo antes de seus próprios efeitos.
|
|
40
|
+
Recibos v1 não são aceitos pelo novo adapter. Não há migração nem adoção automática;
|
|
41
|
+
a futura preparação deve usar a mesma versão do SDK do consumidor. O adapter opt-in
|
|
42
|
+
0.6.12 ainda não está ligado aos consumidores operacionais, portanto este delta não
|
|
43
|
+
altera o provisionador legado nem publica imagens dos membros.
|
|
33
44
|
O preparador deverá usar a mesma função/versão fixada. `receiptName` é
|
|
34
45
|
`oon-preparation-` mais os primeiros 32 hex de SHA256 do JSON canônico de `binding`.
|
|
35
46
|
Objetos são ordenados recursivamente; arrays mantêm ordem. O hash do token também
|
|
@@ -41,7 +52,7 @@ JSON com estes campos:
|
|
|
41
52
|
| Campo | Valor exigido |
|
|
42
53
|
| --- | --- |
|
|
43
54
|
| `phase` | `prepared` |
|
|
44
|
-
| `binding` | Exatamente o retornado pelo SDK: autoridade, identidade, ambiente, release, lease, imagem/digest, deployment, URLs, perfil e hashes das capacidades/token |
|
|
55
|
+
| `binding` | Exatamente o retornado pelo SDK v2: autoridade, identidade, ambiente, release, lease e vencimento, imagem/digest, deployment, URLs, perfil e hashes das capacidades/token |
|
|
45
56
|
| `ingressClass` | `nginx` |
|
|
46
57
|
| `clusterIssuer` | `letsencrypt-production-http01` |
|
|
47
58
|
| `runtimeSecret` | `{name, uid, resourceVersion}` do Secret canônico final |
|
|
@@ -54,6 +65,20 @@ Não reutilizar recibo em outro lease/digest. UID e resourceVersion dos recursos
|
|
|
54
65
|
reconferidos antes de cada escrita e após o rollout. Isso detecta deriva observada,
|
|
55
66
|
mas não substitui um lock distribuído com o preparador.
|
|
56
67
|
|
|
68
|
+
`deployPreparedRuntime` confere o prazo antes e depois de cada leitura, escrita e
|
|
69
|
+
rollout, além de antes do retorno de sucesso (`infraVersion: prepared-member-v2`).
|
|
70
|
+
O orçamento inicial de tempo também é acompanhado por relógio monotônico: um recuo
|
|
71
|
+
do relógio civil durante a chamada não prolonga o prazo. Observação inválida do
|
|
72
|
+
relógio interrompe. O relógio civil inicial do executor precisa estar sincronizado;
|
|
73
|
+
essa checagem local não consulta revogação, comando ou lease na Central.
|
|
74
|
+
|
|
75
|
+
Expiração durante uma requisição não cancela um efeito que já chegou ao servidor.
|
|
76
|
+
O erro `PREPARED_RUNTIME_LEASE_EXPIRED` pode corresponder a escrita parcial; preservar
|
|
77
|
+
e reconciliar os recursos com o comando, sem retry automático, rollback, remoção de
|
|
78
|
+
lock ou takeover. Um novo processo não deve reusar o mesmo lease como autorização
|
|
79
|
+
de concorrência. Exclusão administrativa, fencing/admissão e frescor de callbacks
|
|
80
|
+
permanecem requisitos separados de B5.
|
|
81
|
+
|
|
57
82
|
O runtime Secret deve conter identidade/URLs, credencial de deployment e chave de
|
|
58
83
|
criptografia já preparadas. Mongo exige URI de host único `mongodb`/`mongodb+srv`,
|
|
59
84
|
usuário e banco canônicos e `authSource=admin`; o adapter não prova os privilégios
|