@oondemand/oon-core-back 0.6.7 → 0.6.9
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/bin/delivery.js +5 -2
- package/package.json +1 -1
- package/src/core/technicalIdentity.js +174 -0
- package/src/delivery/mongoProvisioning.js +26 -5
- package/src/delivery/runtime.js +109 -52
- package/src/delivery/technicalIdentity.js +140 -0
- package/src/index.js +3 -1
package/bin/delivery.js
CHANGED
|
@@ -5,6 +5,7 @@ const fs = require("node:fs");
|
|
|
5
5
|
const path = require("node:path");
|
|
6
6
|
const { getContextWithPhase, setState, addEvidence } = require("../src/delivery/api");
|
|
7
7
|
const { provisionRuntime, deployRuntime, waitHttp, activateMember } = require("../src/delivery/runtime");
|
|
8
|
+
const { canonicalizeDeliveryContext } = require("../src/delivery/technicalIdentity");
|
|
8
9
|
|
|
9
10
|
function flag(name, fallback) {
|
|
10
11
|
const args = process.argv.slice(3);
|
|
@@ -16,6 +17,7 @@ function flag(name, fallback) {
|
|
|
16
17
|
}
|
|
17
18
|
function required(value, name) { if (!value) throw new Error(`${name} obrigatório.`); return value; }
|
|
18
19
|
function secret() { return required(process.env.OON_DELIVERY_SHARED_SECRET, "OON_DELIVERY_SHARED_SECRET"); }
|
|
20
|
+
function normalizedContext(value) { return { ...canonicalizeDeliveryContext(value) }; }
|
|
19
21
|
|
|
20
22
|
async function main() {
|
|
21
23
|
const command = process.argv[2];
|
|
@@ -25,7 +27,8 @@ async function main() {
|
|
|
25
27
|
if (command === "context") {
|
|
26
28
|
const phase = flag("--phase", "build");
|
|
27
29
|
const output = required(flag("--output"), "--output");
|
|
28
|
-
const
|
|
30
|
+
const fetched = await getContextWithPhase({ apiUrl: required(apiUrl, "--api"), releaseId: required(releaseId, "--release"), token: secret(), phase });
|
|
31
|
+
const context = normalizedContext(fetched);
|
|
29
32
|
fs.writeFileSync(path.resolve(output), JSON.stringify(context, null, 2), { mode: 0o600 });
|
|
30
33
|
return;
|
|
31
34
|
}
|
|
@@ -40,7 +43,7 @@ async function main() {
|
|
|
40
43
|
if (command === "deploy") {
|
|
41
44
|
const contextFile = required(flag("--context"), "--context");
|
|
42
45
|
const imageDigest = required(flag("--image-digest"), "--image-digest");
|
|
43
|
-
const context = JSON.parse(fs.readFileSync(path.resolve(contextFile), "utf8"));
|
|
46
|
+
const context = normalizedContext(JSON.parse(fs.readFileSync(path.resolve(contextFile), "utf8")));
|
|
44
47
|
let failureState = "falha_infra";
|
|
45
48
|
try {
|
|
46
49
|
await setState({ apiUrl, releaseId, token: secret(), etapa: "provisionando", imageDigest });
|
package/package.json
CHANGED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
|
|
5
|
+
const APP_CODE_MAX_LENGTH = 24;
|
|
6
|
+
const APP_CODE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,22}[a-z0-9])?$/;
|
|
7
|
+
const INSTANCE_UID_ALPHABET = "23456789abcdefghjkmnopqrstuvwxyz";
|
|
8
|
+
const INSTANCE_UID_LENGTH = 16;
|
|
9
|
+
const INSTANCE_UID_BYTES = 10;
|
|
10
|
+
const INSTANCE_UID_PATTERN = /^[23456789abcdefghjkmnopqrstuvwxyz]{16}$/;
|
|
11
|
+
|
|
12
|
+
const ENVIRONMENT_TOKENS = Object.freeze({
|
|
13
|
+
desenvolvimento: "dev",
|
|
14
|
+
homologacao: "hml",
|
|
15
|
+
producao: "prod",
|
|
16
|
+
dev: "dev",
|
|
17
|
+
hml: "hml",
|
|
18
|
+
prod: "prod",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
class TechnicalIdentityError extends Error {
|
|
22
|
+
constructor(message, code, details = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "TechnicalIdentityError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.details = details;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertAppCode(value) {
|
|
31
|
+
const appCode = typeof value === "string" ? value : "";
|
|
32
|
+
if (!APP_CODE_PATTERN.test(appCode)) {
|
|
33
|
+
throw new TechnicalIdentityError(
|
|
34
|
+
`appCode deve seguir ${APP_CODE_PATTERN} e possuir no máximo ${APP_CODE_MAX_LENGTH} caracteres ASCII.`,
|
|
35
|
+
"TECHNICAL_APP_CODE_INVALID",
|
|
36
|
+
{ appCode },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return appCode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertInstanceUid(value) {
|
|
43
|
+
const instanceUid = typeof value === "string" ? value : "";
|
|
44
|
+
if (!INSTANCE_UID_PATTERN.test(instanceUid)) {
|
|
45
|
+
throw new TechnicalIdentityError(
|
|
46
|
+
`instanceUid deve possuir ${INSTANCE_UID_LENGTH} caracteres no alfabeto canônico.`,
|
|
47
|
+
"TECHNICAL_INSTANCE_UID_INVALID",
|
|
48
|
+
{ instanceUid },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return instanceUid;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function environmentToken(value) {
|
|
55
|
+
const environment = String(value || "").trim().toLowerCase();
|
|
56
|
+
const hasEnvironment = Object.prototype.hasOwnProperty.call(ENVIRONMENT_TOKENS, environment);
|
|
57
|
+
const token = hasEnvironment ? ENVIRONMENT_TOKENS[environment] : null;
|
|
58
|
+
if (!token) {
|
|
59
|
+
throw new TechnicalIdentityError(
|
|
60
|
+
"Ambiente técnico inválido.",
|
|
61
|
+
"TECHNICAL_ENVIRONMENT_INVALID",
|
|
62
|
+
{ environment },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function encodeInstanceUid(bytes) {
|
|
69
|
+
const buffer = Buffer.from(bytes || []);
|
|
70
|
+
if (buffer.length !== INSTANCE_UID_BYTES) {
|
|
71
|
+
throw new TechnicalIdentityError(
|
|
72
|
+
`instanceUid exige exatamente ${INSTANCE_UID_BYTES} bytes aleatórios.`,
|
|
73
|
+
"TECHNICAL_INSTANCE_UID_RANDOM_BYTES_INVALID",
|
|
74
|
+
{ receivedBytes: buffer.length },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let accumulator = 0;
|
|
79
|
+
let bits = 0;
|
|
80
|
+
let output = "";
|
|
81
|
+
for (const byte of buffer) {
|
|
82
|
+
accumulator = (accumulator << 8) | byte;
|
|
83
|
+
bits += 8;
|
|
84
|
+
while (bits >= 5) {
|
|
85
|
+
bits -= 5;
|
|
86
|
+
output += INSTANCE_UID_ALPHABET[(accumulator >> bits) & 31];
|
|
87
|
+
accumulator &= (1 << bits) - 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (bits !== 0 || output.length !== INSTANCE_UID_LENGTH) {
|
|
92
|
+
throw new TechnicalIdentityError(
|
|
93
|
+
"Falha ao codificar instanceUid de 80 bits.",
|
|
94
|
+
"TECHNICAL_INSTANCE_UID_ENCODING_INVALID",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return output;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function generateInstanceUid(randomBytes = crypto.randomBytes) {
|
|
101
|
+
if (typeof randomBytes !== "function") {
|
|
102
|
+
throw new TypeError("randomBytes deve ser uma função.");
|
|
103
|
+
}
|
|
104
|
+
return encodeInstanceUid(randomBytes(INSTANCE_UID_BYTES));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function technicalIdentity({ appCode, instanceUid, environment }) {
|
|
108
|
+
const canonicalAppCode = assertAppCode(appCode);
|
|
109
|
+
const canonicalInstanceUid = assertInstanceUid(instanceUid);
|
|
110
|
+
const env = environmentToken(environment);
|
|
111
|
+
const base = `${canonicalAppCode}-${canonicalInstanceUid}-${env}`;
|
|
112
|
+
const database = `${canonicalAppCode}_${canonicalInstanceUid}_${env}`;
|
|
113
|
+
const mongoUsername = `${base}-rw`;
|
|
114
|
+
const serviceAccount = `${base}-runtime`;
|
|
115
|
+
const secret = `${base}-runtime`;
|
|
116
|
+
const provisionJob = `${base}-provision`;
|
|
117
|
+
const frontendDnsLabel = base;
|
|
118
|
+
const backendDnsLabel = `${base}-api`;
|
|
119
|
+
const tlsSecret = `${base}-tls`;
|
|
120
|
+
|
|
121
|
+
const lengths = {
|
|
122
|
+
base: base.length,
|
|
123
|
+
database: database.length,
|
|
124
|
+
mongoUsername: mongoUsername.length,
|
|
125
|
+
serviceAccount: serviceAccount.length,
|
|
126
|
+
secret: secret.length,
|
|
127
|
+
provisionJob: provisionJob.length,
|
|
128
|
+
frontendDnsLabel: frontendDnsLabel.length,
|
|
129
|
+
backendDnsLabel: backendDnsLabel.length,
|
|
130
|
+
tlsSecret: tlsSecret.length,
|
|
131
|
+
};
|
|
132
|
+
const invalidLength = Object.entries(lengths).find(([, length]) => length > 63);
|
|
133
|
+
if (invalidLength) {
|
|
134
|
+
throw new TechnicalIdentityError(
|
|
135
|
+
"Identidade técnica excede o orçamento DNS/Kubernetes de 63 caracteres.",
|
|
136
|
+
"TECHNICAL_IDENTITY_BUDGET_EXCEEDED",
|
|
137
|
+
{ resource: invalidLength[0], length: invalidLength[1] },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
version: 1,
|
|
143
|
+
appCode: canonicalAppCode,
|
|
144
|
+
instanceUid: canonicalInstanceUid,
|
|
145
|
+
environment: env,
|
|
146
|
+
resourceBase: base,
|
|
147
|
+
namespace: base,
|
|
148
|
+
database,
|
|
149
|
+
mongoUsername,
|
|
150
|
+
serviceAccount,
|
|
151
|
+
secret,
|
|
152
|
+
provisionJob,
|
|
153
|
+
frontendDnsLabel,
|
|
154
|
+
backendDnsLabel,
|
|
155
|
+
tlsSecret,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = Object.freeze({
|
|
160
|
+
APP_CODE_MAX_LENGTH,
|
|
161
|
+
APP_CODE_PATTERN,
|
|
162
|
+
INSTANCE_UID_ALPHABET,
|
|
163
|
+
INSTANCE_UID_LENGTH,
|
|
164
|
+
INSTANCE_UID_BYTES,
|
|
165
|
+
INSTANCE_UID_PATTERN,
|
|
166
|
+
ENVIRONMENT_TOKENS,
|
|
167
|
+
TechnicalIdentityError,
|
|
168
|
+
assertAppCode,
|
|
169
|
+
assertInstanceUid,
|
|
170
|
+
environmentToken,
|
|
171
|
+
encodeInstanceUid,
|
|
172
|
+
generateInstanceUid,
|
|
173
|
+
technicalIdentity,
|
|
174
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { mongo } = require("mongoose");
|
|
4
|
+
const { technicalIdentity } = require("../core/technicalIdentity");
|
|
4
5
|
|
|
5
6
|
const DIGITALOCEAN_API = "https://api.digitalocean.com/v2";
|
|
6
7
|
const DIGITALOCEAN_HOST_SUFFIX = ".mongo.ondigitalocean.com";
|
|
@@ -24,8 +25,19 @@ function dnsLabel(value, max = 63) {
|
|
|
24
25
|
.replace(/-+$/, "");
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function mongoIdentity(appCode, environment) {
|
|
28
|
+
function mongoIdentity(appCode, environment, { instanceUid } = {}) {
|
|
29
|
+
if (instanceUid != null) {
|
|
30
|
+
const identity = technicalIdentity({ appCode, instanceUid, environment });
|
|
31
|
+
return {
|
|
32
|
+
mode: "canonical",
|
|
33
|
+
identityVersion: identity.version,
|
|
34
|
+
instanceUid: identity.instanceUid,
|
|
35
|
+
dbName: identity.database,
|
|
36
|
+
username: identity.mongoUsername,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
28
39
|
return {
|
|
40
|
+
mode: "legacy",
|
|
29
41
|
dbName: dnsLabel(`${appCode}_${environment}`, 48).replace(/-/g, "_"),
|
|
30
42
|
username: dnsLabel(`${appCode}-${environment}`, 48),
|
|
31
43
|
};
|
|
@@ -129,8 +141,9 @@ async function waitForMongoUri(
|
|
|
129
141
|
);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
async function provisionNativeMongo({ adminUri, appCode, environment }) {
|
|
133
|
-
const
|
|
144
|
+
async function provisionNativeMongo({ adminUri, appCode, environment, instanceUid }) {
|
|
145
|
+
const identity = mongoIdentity(appCode, environment, { instanceUid });
|
|
146
|
+
const { dbName, username } = identity;
|
|
134
147
|
const password = randomSecret(30);
|
|
135
148
|
const client = new mongo.MongoClient(adminUri, {
|
|
136
149
|
serverSelectionTimeoutMS: 15000,
|
|
@@ -163,6 +176,9 @@ async function provisionNativeMongo({ adminUri, appCode, environment }) {
|
|
|
163
176
|
return {
|
|
164
177
|
dbName,
|
|
165
178
|
username,
|
|
179
|
+
...(identity.mode === "canonical"
|
|
180
|
+
? { instanceUid: identity.instanceUid, technicalIdentityVersion: identity.identityVersion }
|
|
181
|
+
: {}),
|
|
166
182
|
mongoUri: buildRuntimeMongoUri({
|
|
167
183
|
adminUri,
|
|
168
184
|
username,
|
|
@@ -383,6 +399,7 @@ async function provisionDigitalOceanMongo(
|
|
|
383
399
|
adminUri,
|
|
384
400
|
appCode,
|
|
385
401
|
environment,
|
|
402
|
+
instanceUid,
|
|
386
403
|
digitalOceanToken,
|
|
387
404
|
digitalOceanClusterId,
|
|
388
405
|
},
|
|
@@ -399,7 +416,8 @@ async function provisionDigitalOceanMongo(
|
|
|
399
416
|
);
|
|
400
417
|
}
|
|
401
418
|
|
|
402
|
-
const
|
|
419
|
+
const identity = mongoIdentity(appCode, environment, { instanceUid });
|
|
420
|
+
const { dbName, username } = identity;
|
|
403
421
|
const apiOptions = {
|
|
404
422
|
token: digitalOceanToken,
|
|
405
423
|
clusterId: digitalOceanClusterId,
|
|
@@ -425,6 +443,9 @@ async function provisionDigitalOceanMongo(
|
|
|
425
443
|
return {
|
|
426
444
|
dbName,
|
|
427
445
|
username: user.name || username,
|
|
446
|
+
...(identity.mode === "canonical"
|
|
447
|
+
? { instanceUid: identity.instanceUid, technicalIdentityVersion: identity.identityVersion }
|
|
448
|
+
: {}),
|
|
428
449
|
mongoUri,
|
|
429
450
|
provider: "digitalocean",
|
|
430
451
|
};
|
|
@@ -462,4 +483,4 @@ module.exports = {
|
|
|
462
483
|
provisionDigitalOceanUser,
|
|
463
484
|
provisionDigitalOceanMongo,
|
|
464
485
|
provisionMongo,
|
|
465
|
-
};
|
|
486
|
+
};
|
package/src/delivery/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ const { spawnSync } = require("node:child_process");
|
|
|
5
5
|
const { kubectl } = require("./command");
|
|
6
6
|
const { validateInfraFromCluster } = require("../deploy/contracts/infra-contract");
|
|
7
7
|
const { provisionMongo, validateMongoUri } = require("./mongoProvisioning");
|
|
8
|
+
const { canonicalizeDeliveryContext } = require("./technicalIdentity");
|
|
8
9
|
|
|
9
10
|
const RESOURCE_PROFILES = {
|
|
10
11
|
nano: { cpuRequest: "50m", cpuLimit: "250m", memoryRequest: "96Mi", memoryLimit: "256Mi" },
|
|
@@ -36,24 +37,40 @@ function readSecret(namespace, name, key) {
|
|
|
36
37
|
} catch { return null; }
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function canonicalRuntimeContext(context) {
|
|
41
|
+
return canonicalizeDeliveryContext(context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function identityLabels(identity) {
|
|
45
|
+
if (identity.mode !== "canonical") return "";
|
|
46
|
+
return `\n oon.runtime/instance-uid: ${identity.instanceUid}\n oon.runtime/identity-version: \"${identity.version}\"`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function podIdentityLabels(identity) {
|
|
50
|
+
if (identity.mode !== "canonical") return "";
|
|
51
|
+
return `\n oon.runtime/instance-uid: ${identity.instanceUid}\n oon.runtime/identity-version: \"${identity.version}\"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function baselineManifest({ context }) {
|
|
55
|
+
const normalized = canonicalRuntimeContext(context);
|
|
56
|
+
const identity = normalized.deliveryTechnicalIdentity;
|
|
40
57
|
return `apiVersion: v1
|
|
41
58
|
kind: Namespace
|
|
42
59
|
metadata:
|
|
43
|
-
name: ${
|
|
60
|
+
name: ${normalized.namespace}
|
|
44
61
|
labels:
|
|
45
|
-
oon.dev/app-code: ${
|
|
46
|
-
oon.dev/environment: ${
|
|
47
|
-
app.kubernetes.io/managed-by: oon-platform
|
|
62
|
+
oon.dev/app-code: ${normalized.appCode}
|
|
63
|
+
oon.dev/environment: ${normalized.environment}
|
|
64
|
+
app.kubernetes.io/managed-by: oon-platform${identityLabels(identity)}
|
|
48
65
|
---
|
|
49
66
|
apiVersion: v1
|
|
50
67
|
kind: ServiceAccount
|
|
51
|
-
metadata: { name:
|
|
68
|
+
metadata: { name: ${identity.serviceAccount}, namespace: ${normalized.namespace} }
|
|
52
69
|
automountServiceAccountToken: false
|
|
53
70
|
---
|
|
54
71
|
apiVersion: v1
|
|
55
72
|
kind: LimitRange
|
|
56
|
-
metadata: { name: oon-runtime-defaults, namespace: ${
|
|
73
|
+
metadata: { name: oon-runtime-defaults, namespace: ${normalized.namespace} }
|
|
57
74
|
spec:
|
|
58
75
|
limits:
|
|
59
76
|
- type: Container
|
|
@@ -62,13 +79,13 @@ spec:
|
|
|
62
79
|
---
|
|
63
80
|
apiVersion: v1
|
|
64
81
|
kind: ResourceQuota
|
|
65
|
-
metadata: { name: oon-runtime-quota, namespace: ${
|
|
82
|
+
metadata: { name: oon-runtime-quota, namespace: ${normalized.namespace} }
|
|
66
83
|
spec:
|
|
67
84
|
hard: { requests.cpu: "2", requests.memory: 2Gi, limits.cpu: "4", limits.memory: 4Gi, pods: "20", services: "10" }
|
|
68
85
|
---
|
|
69
86
|
apiVersion: networking.k8s.io/v1
|
|
70
87
|
kind: NetworkPolicy
|
|
71
|
-
metadata: { name: oon-runtime-baseline, namespace: ${
|
|
88
|
+
metadata: { name: oon-runtime-baseline, namespace: ${normalized.namespace} }
|
|
72
89
|
spec:
|
|
73
90
|
podSelector: {}
|
|
74
91
|
policyTypes: [Ingress, Egress]
|
|
@@ -82,35 +99,37 @@ spec:
|
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
function runtimeManifest({ context, infra, imageRef, profile = "small" }) {
|
|
102
|
+
const normalized = canonicalRuntimeContext(context);
|
|
103
|
+
const identity = normalized.deliveryTechnicalIdentity;
|
|
85
104
|
const resources = RESOURCE_PROFILES[profile] || RESOURCE_PROFILES.small;
|
|
86
|
-
const releaseLabel = kubernetesLabelValue(
|
|
87
|
-
const releaseAnnotation = JSON.stringify(String(
|
|
105
|
+
const releaseLabel = kubernetesLabelValue(normalized.releaseId);
|
|
106
|
+
const releaseAnnotation = JSON.stringify(String(normalized.releaseId));
|
|
88
107
|
return `apiVersion: apps/v1
|
|
89
108
|
kind: Deployment
|
|
90
109
|
metadata:
|
|
91
|
-
name: ${
|
|
92
|
-
namespace: ${
|
|
110
|
+
name: ${normalized.appCode}
|
|
111
|
+
namespace: ${normalized.namespace}
|
|
93
112
|
spec:
|
|
94
113
|
replicas: 1
|
|
95
114
|
selector:
|
|
96
|
-
matchLabels: { app.kubernetes.io/name: ${
|
|
115
|
+
matchLabels: { app.kubernetes.io/name: ${normalized.appCode} }
|
|
97
116
|
template:
|
|
98
117
|
metadata:
|
|
99
118
|
labels:
|
|
100
|
-
app.kubernetes.io/name: ${
|
|
119
|
+
app.kubernetes.io/name: ${normalized.appCode}
|
|
101
120
|
app.kubernetes.io/part-of: oon-central
|
|
102
|
-
oon.dev/release-id: ${releaseLabel}
|
|
121
|
+
oon.dev/release-id: ${releaseLabel}${podIdentityLabels(identity)}
|
|
103
122
|
annotations:
|
|
104
123
|
oon.dev/release-id: ${releaseAnnotation}
|
|
105
124
|
spec:
|
|
106
|
-
serviceAccountName:
|
|
125
|
+
serviceAccountName: ${identity.serviceAccount}
|
|
107
126
|
imagePullSecrets: [{ name: ${infra.REGISTRY_PULL_SECRET_NAME || "ghcr-oon"} }]
|
|
108
127
|
containers:
|
|
109
128
|
- name: central
|
|
110
129
|
image: ${imageRef}
|
|
111
130
|
imagePullPolicy: IfNotPresent
|
|
112
131
|
ports: [{ name: http, containerPort: 8080 }]
|
|
113
|
-
envFrom: [{ secretRef: { name:
|
|
132
|
+
envFrom: [{ secretRef: { name: ${identity.secret} } }]
|
|
114
133
|
resources:
|
|
115
134
|
requests: { cpu: ${resources.cpuRequest}, memory: ${resources.memoryRequest} }
|
|
116
135
|
limits: { cpu: ${resources.cpuLimit}, memory: ${resources.memoryLimit} }
|
|
@@ -123,28 +142,28 @@ spec:
|
|
|
123
142
|
---
|
|
124
143
|
apiVersion: v1
|
|
125
144
|
kind: Service
|
|
126
|
-
metadata: { name: ${
|
|
145
|
+
metadata: { name: ${normalized.appCode}, namespace: ${normalized.namespace} }
|
|
127
146
|
spec:
|
|
128
|
-
selector: { app.kubernetes.io/name: ${
|
|
147
|
+
selector: { app.kubernetes.io/name: ${normalized.appCode} }
|
|
129
148
|
ports: [{ name: http, port: 80, targetPort: http }]
|
|
130
149
|
---
|
|
131
150
|
apiVersion: networking.k8s.io/v1
|
|
132
151
|
kind: Ingress
|
|
133
152
|
metadata:
|
|
134
|
-
name: ${
|
|
135
|
-
namespace: ${
|
|
153
|
+
name: ${normalized.appCode}
|
|
154
|
+
namespace: ${normalized.namespace}
|
|
136
155
|
annotations: { cert-manager.io/cluster-issuer: ${infra.CLUSTER_ISSUER} }
|
|
137
156
|
spec:
|
|
138
157
|
ingressClassName: ${infra.INGRESS_CLASS}
|
|
139
|
-
tls: [{ hosts: [${
|
|
158
|
+
tls: [{ hosts: [${normalized.hostname}], secretName: ${identity.tlsSecret} }]
|
|
140
159
|
rules:
|
|
141
|
-
- host: ${
|
|
160
|
+
- host: ${normalized.hostname}
|
|
142
161
|
http:
|
|
143
162
|
paths:
|
|
144
163
|
- path: /
|
|
145
164
|
pathType: Prefix
|
|
146
165
|
backend:
|
|
147
|
-
service: { name: ${
|
|
166
|
+
service: { name: ${normalized.appCode}, port: { name: http } }
|
|
148
167
|
`;
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -159,26 +178,36 @@ function copyPullSecret(namespace, infra) {
|
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
function applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets = {} }) {
|
|
181
|
+
const normalized = canonicalRuntimeContext(context);
|
|
182
|
+
const identity = normalized.deliveryTechnicalIdentity;
|
|
162
183
|
const literals = {
|
|
163
184
|
MONGO_URI: mongoUri,
|
|
164
185
|
NODE_ENV: "production",
|
|
165
|
-
APP_ENVIRONMENT:
|
|
166
|
-
APP_CODE:
|
|
167
|
-
PUBLIC_APP_URL:
|
|
168
|
-
CENTRAL_ATIVACAO_URL:
|
|
169
|
-
CENTRAL_ATIVACAO_API_URL:
|
|
186
|
+
APP_ENVIRONMENT: normalized.environment,
|
|
187
|
+
APP_CODE: normalized.appCode,
|
|
188
|
+
PUBLIC_APP_URL: normalized.publicUrl,
|
|
189
|
+
CENTRAL_ATIVACAO_URL: normalized.activationApiUrl,
|
|
190
|
+
CENTRAL_ATIVACAO_API_URL: normalized.activationApiUrl,
|
|
170
191
|
INSTANCE_CREDENTIAL_ENCRYPTION_KEY: encryptionKey,
|
|
192
|
+
...(identity.mode === "canonical"
|
|
193
|
+
? {
|
|
194
|
+
OON_INSTANCE_UID: identity.instanceUid,
|
|
195
|
+
OON_TECHNICAL_IDENTITY_VERSION: String(identity.version),
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
171
198
|
...extraSecrets,
|
|
172
199
|
};
|
|
173
|
-
const args = ["-n",
|
|
200
|
+
const args = ["-n", normalized.namespace, "create", "secret", "generic", identity.secret];
|
|
174
201
|
for (const [key, value] of Object.entries(literals)) args.push(`--from-literal=${key}=${value}`);
|
|
175
202
|
args.push("--dry-run=client", "-o", "yaml");
|
|
176
203
|
applyYaml(kubectl(args, { capture: true }));
|
|
177
204
|
}
|
|
178
205
|
|
|
179
206
|
async function provisionRuntime({ context, extraSecrets = {} }) {
|
|
207
|
+
const normalized = canonicalRuntimeContext(context);
|
|
208
|
+
const identity = normalized.deliveryTechnicalIdentity;
|
|
180
209
|
const infra = validateInfraFromCluster().config;
|
|
181
|
-
applyYaml(baselineManifest({ context, infra }));
|
|
210
|
+
applyYaml(baselineManifest({ context: normalized, infra }));
|
|
182
211
|
|
|
183
212
|
const platformNamespace = infra.PLATFORM_NAMESPACE || "oon-system";
|
|
184
213
|
const adminSecretName = infra.MONGO_ADMIN_SECRET_NAME || "oon-mongo-admin";
|
|
@@ -205,7 +234,7 @@ async function provisionRuntime({ context, extraSecrets = {} }) {
|
|
|
205
234
|
"DIGITALOCEAN_DATABASE_CLUSTER_ID",
|
|
206
235
|
);
|
|
207
236
|
|
|
208
|
-
let mongoUri = readSecret(
|
|
237
|
+
let mongoUri = readSecret(normalized.namespace, identity.secret, "MONGO_URI");
|
|
209
238
|
let databaseName = "";
|
|
210
239
|
let credentialRotated = false;
|
|
211
240
|
|
|
@@ -225,8 +254,9 @@ async function provisionRuntime({ context, extraSecrets = {} }) {
|
|
|
225
254
|
if (!mongoUri) {
|
|
226
255
|
const provisioned = await provisionMongo({
|
|
227
256
|
adminUri,
|
|
228
|
-
appCode:
|
|
229
|
-
environment:
|
|
257
|
+
appCode: normalized.appCode,
|
|
258
|
+
environment: normalized.environment,
|
|
259
|
+
...(identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}),
|
|
230
260
|
provisioner: mongoProvisioner,
|
|
231
261
|
digitalOceanToken,
|
|
232
262
|
digitalOceanClusterId,
|
|
@@ -238,20 +268,32 @@ async function provisionRuntime({ context, extraSecrets = {} }) {
|
|
|
238
268
|
|
|
239
269
|
const encryptionKey =
|
|
240
270
|
readSecret(
|
|
241
|
-
|
|
242
|
-
|
|
271
|
+
normalized.namespace,
|
|
272
|
+
identity.secret,
|
|
243
273
|
"INSTANCE_CREDENTIAL_ENCRYPTION_KEY",
|
|
244
274
|
) || randomSecret(48);
|
|
245
|
-
applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets });
|
|
246
|
-
return {
|
|
275
|
+
applyRuntimeSecret({ context: normalized, mongoUri, encryptionKey, extraSecrets });
|
|
276
|
+
return {
|
|
277
|
+
infra,
|
|
278
|
+
databaseName,
|
|
279
|
+
credentialRotated,
|
|
280
|
+
...(identity.mode === "canonical" ? { technicalIdentity: identity } : {}),
|
|
281
|
+
};
|
|
247
282
|
}
|
|
248
283
|
|
|
249
284
|
function deployRuntime({ context, infra, imageDigest, resourceProfile = "small" }) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
285
|
+
const normalized = canonicalRuntimeContext(context);
|
|
286
|
+
copyPullSecret(normalized.namespace, infra);
|
|
287
|
+
const imageRef = `${normalized.imageRepository}@${imageDigest}`;
|
|
288
|
+
applyYaml(runtimeManifest({ context: normalized, infra, imageRef, profile: resourceProfile }));
|
|
289
|
+
kubectl(["-n", normalized.namespace, "rollout", "status", `deployment/${normalized.appCode}`, "--timeout=10m"]);
|
|
290
|
+
return {
|
|
291
|
+
infraVersion: infra.INFRA_SCHEMA_VERSION,
|
|
292
|
+
imageRef,
|
|
293
|
+
...(normalized.deliveryTechnicalIdentity.mode === "canonical"
|
|
294
|
+
? { technicalIdentity: normalized.deliveryTechnicalIdentity }
|
|
295
|
+
: {}),
|
|
296
|
+
};
|
|
255
297
|
}
|
|
256
298
|
|
|
257
299
|
async function waitHttp(url, { timeoutMs = 300000, expectedStatus = 200 } = {}) {
|
|
@@ -269,19 +311,20 @@ async function waitHttp(url, { timeoutMs = 300000, expectedStatus = 200 } = {})
|
|
|
269
311
|
}
|
|
270
312
|
|
|
271
313
|
async function activateMember(context) {
|
|
272
|
-
const
|
|
314
|
+
const normalized = canonicalRuntimeContext(context);
|
|
315
|
+
const statusResponse = await waitHttp(`${normalized.publicUrl}/api/ativacao/status`);
|
|
273
316
|
const status = await statusResponse.json();
|
|
274
317
|
if (status.status === "ativa") return { alreadyActive: true, instanceId: status.instanceId };
|
|
275
|
-
if (!
|
|
276
|
-
const response = await fetch(`${
|
|
318
|
+
if (!normalized.activationCode) throw new Error("Instância não está ativa e não há código de ativação autorizado.");
|
|
319
|
+
const response = await fetch(`${normalized.publicUrl}/api/ativacao/concluir`, {
|
|
277
320
|
method: "POST",
|
|
278
|
-
headers: { "Content-Type": "application/json", "Idempotency-Key":
|
|
321
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": normalized.releaseId },
|
|
279
322
|
body: JSON.stringify({
|
|
280
|
-
codigoAtivacao:
|
|
281
|
-
nomeInstancia: `${
|
|
323
|
+
codigoAtivacao: normalized.activationCode,
|
|
324
|
+
nomeInstancia: `${normalized.appCode} dev`,
|
|
282
325
|
parametrosGlobais: {
|
|
283
|
-
nomeInstancia: `${
|
|
284
|
-
urlPublica:
|
|
326
|
+
nomeInstancia: `${normalized.appCode} dev`, ambiente: normalized.environment,
|
|
327
|
+
urlPublica: normalized.publicUrl, emailAdministrativo: normalized.administrativeEmail,
|
|
285
328
|
idioma: "pt-BR", timezone: "America/Sao_Paulo", moeda: "BRL", pais: "BR", formatoData: "DD/MM/YYYY",
|
|
286
329
|
},
|
|
287
330
|
parametrosAplicacao: {},
|
|
@@ -292,4 +335,18 @@ async function activateMember(context) {
|
|
|
292
335
|
return data;
|
|
293
336
|
}
|
|
294
337
|
|
|
295
|
-
module.exports = {
|
|
338
|
+
module.exports = {
|
|
339
|
+
RESOURCE_PROFILES,
|
|
340
|
+
randomSecret,
|
|
341
|
+
dnsLabel,
|
|
342
|
+
kubernetesLabelValue,
|
|
343
|
+
canonicalRuntimeContext,
|
|
344
|
+
provisionMongo,
|
|
345
|
+
validateMongoUri,
|
|
346
|
+
baselineManifest,
|
|
347
|
+
runtimeManifest,
|
|
348
|
+
provisionRuntime,
|
|
349
|
+
deployRuntime,
|
|
350
|
+
waitHttp,
|
|
351
|
+
activateMember,
|
|
352
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const technical = require("../core/technicalIdentity");
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_DELIVERY_IDENTITY_VERSION = 1;
|
|
6
|
+
const CANONICAL_FIELDS = Object.freeze([
|
|
7
|
+
"version",
|
|
8
|
+
"appCode",
|
|
9
|
+
"instanceUid",
|
|
10
|
+
"environment",
|
|
11
|
+
"resourceBase",
|
|
12
|
+
"namespace",
|
|
13
|
+
"database",
|
|
14
|
+
"mongoUsername",
|
|
15
|
+
"serviceAccount",
|
|
16
|
+
"secret",
|
|
17
|
+
"provisionJob",
|
|
18
|
+
"frontendDnsLabel",
|
|
19
|
+
"backendDnsLabel",
|
|
20
|
+
"tlsSecret",
|
|
21
|
+
]);
|
|
22
|
+
const CANONICAL_SIGNAL_FIELDS = Object.freeze([
|
|
23
|
+
"instanceUid",
|
|
24
|
+
"technicalIdentityVersion",
|
|
25
|
+
"technicalInstanceId",
|
|
26
|
+
"technicalIdentity",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function deliveryIdentityError(code, message, details = {}) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.name = "DeliveryTechnicalIdentityError";
|
|
32
|
+
error.code = code;
|
|
33
|
+
error.details = details;
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasOwn(object, field) {
|
|
38
|
+
return Object.prototype.hasOwnProperty.call(object || {}, field);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasCanonicalIdentitySignal(context = {}) {
|
|
42
|
+
return CANONICAL_SIGNAL_FIELDS.some((field) => hasOwn(context, field));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function legacyDeliveryIdentity(context = {}) {
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
mode: "legacy",
|
|
48
|
+
version: 0,
|
|
49
|
+
appCode: String(context.appCode || ""),
|
|
50
|
+
environment: String(context.environment || ""),
|
|
51
|
+
namespace: String(context.namespace || ""),
|
|
52
|
+
serviceAccount: "oon-runtime",
|
|
53
|
+
secret: "oon-runtime",
|
|
54
|
+
tlsSecret: `${String(context.appCode || "")}-tls`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertProjection(canonical, context = {}) {
|
|
59
|
+
const mismatches = [];
|
|
60
|
+
if (context.namespace != null && String(context.namespace) !== canonical.namespace) {
|
|
61
|
+
mismatches.push("namespace");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (hasOwn(context, "technicalIdentity")) {
|
|
65
|
+
const projected = context.technicalIdentity;
|
|
66
|
+
if (!projected || typeof projected !== "object" || Array.isArray(projected)) {
|
|
67
|
+
throw deliveryIdentityError(
|
|
68
|
+
"DELIVERY_TECHNICAL_IDENTITY_PROJECTION_INVALID",
|
|
69
|
+
"A projeção da identidade técnica do delivery é inválida.",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
for (const field of CANONICAL_FIELDS) {
|
|
73
|
+
if (projected[field] == null) continue;
|
|
74
|
+
if (String(projected[field]) !== String(canonical[field])) mismatches.push(field);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (mismatches.length) {
|
|
79
|
+
throw deliveryIdentityError(
|
|
80
|
+
"DELIVERY_TECHNICAL_IDENTITY_MISMATCH",
|
|
81
|
+
"O contexto de delivery diverge da identidade técnica canônica.",
|
|
82
|
+
{ mismatches: [...new Set(mismatches)] },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveDeliveryTechnicalIdentity(context = {}, { requireCanonical = false } = {}) {
|
|
88
|
+
if (!hasCanonicalIdentitySignal(context)) {
|
|
89
|
+
if (requireCanonical) {
|
|
90
|
+
throw deliveryIdentityError(
|
|
91
|
+
"DELIVERY_TECHNICAL_IDENTITY_REQUIRED",
|
|
92
|
+
"O contexto de delivery exige identidade técnica canônica.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return legacyDeliveryIdentity(context);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (context.instanceUid == null || context.technicalIdentityVersion == null) {
|
|
99
|
+
throw deliveryIdentityError(
|
|
100
|
+
"DELIVERY_TECHNICAL_IDENTITY_INCOMPLETE",
|
|
101
|
+
"O contexto de delivery possui identidade técnica parcial.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (Number(context.technicalIdentityVersion) !== SUPPORTED_DELIVERY_IDENTITY_VERSION) {
|
|
105
|
+
throw deliveryIdentityError(
|
|
106
|
+
"DELIVERY_TECHNICAL_IDENTITY_VERSION_UNSUPPORTED",
|
|
107
|
+
"A versão da identidade técnica do delivery não é suportada.",
|
|
108
|
+
{ received: context.technicalIdentityVersion, supported: SUPPORTED_DELIVERY_IDENTITY_VERSION },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const canonical = technical.technicalIdentity({
|
|
113
|
+
appCode: context.appCode,
|
|
114
|
+
instanceUid: context.instanceUid,
|
|
115
|
+
environment: context.environment,
|
|
116
|
+
});
|
|
117
|
+
assertProjection(canonical, context);
|
|
118
|
+
return Object.freeze({ mode: "canonical", ...canonical });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function canonicalizeDeliveryContext(context = {}, options = {}) {
|
|
122
|
+
const identity = resolveDeliveryTechnicalIdentity(context, options);
|
|
123
|
+
if (identity.mode !== "canonical") {
|
|
124
|
+
return Object.freeze({ ...context, deliveryTechnicalIdentity: identity });
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
...context,
|
|
128
|
+
namespace: identity.namespace,
|
|
129
|
+
deliveryTechnicalIdentity: identity,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = Object.freeze({
|
|
134
|
+
CANONICAL_FIELDS,
|
|
135
|
+
CANONICAL_SIGNAL_FIELDS,
|
|
136
|
+
SUPPORTED_DELIVERY_IDENTITY_VERSION,
|
|
137
|
+
canonicalizeDeliveryContext,
|
|
138
|
+
hasCanonicalIdentitySignal,
|
|
139
|
+
resolveDeliveryTechnicalIdentity,
|
|
140
|
+
});
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const tenancy = require("./core/tenancy");
|
|
|
10
10
|
const GenericError = require("./core/errors/GenericError");
|
|
11
11
|
const { requirePermission } = require("./core/middlewares/permissions");
|
|
12
12
|
const rbacPolicy = require("./core/rbac/policy");
|
|
13
|
+
const technicalIdentity = require("./core/technicalIdentity");
|
|
13
14
|
const capabilities = require("./capabilities");
|
|
14
15
|
const { operationalRequestHeaders } = require("./core/utils/instanceActivation");
|
|
15
16
|
const appManifest = require("./manifest/appManifest");
|
|
@@ -75,6 +76,7 @@ module.exports = {
|
|
|
75
76
|
drainProcessJobs: processRuntime.drainProcessJobs,
|
|
76
77
|
fields,
|
|
77
78
|
capabilities,
|
|
79
|
+
technicalIdentity,
|
|
78
80
|
operationalRequestHeaders,
|
|
79
81
|
licensing: {
|
|
80
82
|
assertBusinessOperation: require("./core/licensing").assertBusinessOperation,
|
|
@@ -85,4 +87,4 @@ module.exports = {
|
|
|
85
87
|
PdfRenderingError: capabilities.PdfRenderingError,
|
|
86
88
|
TransactionalEmailError: capabilities.TransactionalEmailError,
|
|
87
89
|
GenericError, registry,
|
|
88
|
-
};
|
|
90
|
+
};
|