@abgov/nx-oc 13.0.0-beta.2 → 13.0.0-beta.4
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/executors.json +5 -0
- package/migrations.json +10 -0
- package/package.json +4 -1
- package/src/executors/sandbox/sandbox.d.ts +5 -0
- package/src/executors/sandbox/sandbox.js +217 -0
- package/src/executors/sandbox/sandbox.js.map +1 -0
- package/src/executors/sandbox/sandbox.spec.ts +223 -0
- package/src/executors/sandbox/schema.d.ts +13 -0
- package/src/executors/sandbox/schema.json +55 -0
- package/src/generators/sandbox/files/SANDBOX.md__tmpl__ +99 -0
- package/src/generators/sandbox/sandbox.js +26 -89
- package/src/generators/sandbox/sandbox.js.map +1 -1
- package/src/generators/sandbox/sandbox.spec.ts +49 -89
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.d.ts +2 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.js +54 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.js.map +1 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.spec.ts +111 -0
package/executors.json
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
"implementation": "./src/executors/apply/apply",
|
|
6
6
|
"schema": "./src/executors/apply/schema.json",
|
|
7
7
|
"description": "Executor that applies deployment template to OpenShift."
|
|
8
|
+
},
|
|
9
|
+
"sandbox": {
|
|
10
|
+
"implementation": "./src/executors/sandbox/sandbox",
|
|
11
|
+
"schema": "./src/executors/sandbox/schema.json",
|
|
12
|
+
"description": "Build locally with podman, push to the registry, import into the sandbox namespace, and roll out."
|
|
8
13
|
}
|
|
9
14
|
}
|
|
10
15
|
}
|
package/migrations.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"generators": {
|
|
4
|
+
"convert-sandbox-target-to-executor": {
|
|
5
|
+
"version": "13.0.0-beta.3",
|
|
6
|
+
"description": "Convert generated sandbox `nx:run-commands` targets to the @abgov/nx-oc:sandbox executor.",
|
|
7
|
+
"implementation": "./src/migrations/convert-sandbox-target/convert-sandbox-target"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abgov/nx-oc",
|
|
3
|
-
"version": "13.0.0-beta.
|
|
3
|
+
"version": "13.0.0-beta.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"description": "Government of Alberta - Nx plugin for OpenShift.",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
},
|
|
24
24
|
"generators": "./generators.json",
|
|
25
25
|
"executors": "./executors.json",
|
|
26
|
+
"nx-migrations": {
|
|
27
|
+
"migrations": "./migrations.json"
|
|
28
|
+
},
|
|
26
29
|
"scripts": {},
|
|
27
30
|
"types": "./src/index.d.ts",
|
|
28
31
|
"type": "commonjs"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = runExecutor;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const app_type_1 = require("../../utils/app-type");
|
|
8
|
+
const oc_utils_1 = require("../../utils/oc-utils");
|
|
9
|
+
const PROXY_TAG_PREFIX = 'adsp:proxy-service:';
|
|
10
|
+
// Fail fast, with an actionable message, before the (slow) production build —
|
|
11
|
+
// rather than a raw "command not found" partway through.
|
|
12
|
+
function requireTool(tool, hint) {
|
|
13
|
+
try {
|
|
14
|
+
(0, child_process_1.execSync)(`command -v ${tool}`, { stdio: 'ignore', shell: '/bin/bash' });
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
throw new Error(`'${tool}' is required but was not found on PATH. ${hint}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// The pull secret + registry login read the gh session token, so gh must be
|
|
21
|
+
// installed AND authenticated. Checked up front so a missing/expired login
|
|
22
|
+
// fails before the slow build rather than at the push/import step.
|
|
23
|
+
function requireGhAuth() {
|
|
24
|
+
requireTool('gh', 'Install the GitHub CLI (https://cli.github.com).');
|
|
25
|
+
try {
|
|
26
|
+
(0, child_process_1.execSync)('gh auth status', { stdio: 'ignore', shell: '/bin/bash' });
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
throw new Error('gh is installed but not authenticated. Run `gh auth login` as an account with write:packages on the registry org (check/switch with `gh auth status` / `gh auth switch`).');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// podman must be installed and its machine reachable. `podman info` fails when
|
|
33
|
+
// the machine is stopped, so it catches that before the build.
|
|
34
|
+
function requirePodman() {
|
|
35
|
+
requireTool('podman', "Install it (macOS: 'brew install podman').");
|
|
36
|
+
try {
|
|
37
|
+
(0, child_process_1.execSync)('podman info', { stdio: 'ignore', shell: '/bin/bash' });
|
|
38
|
+
}
|
|
39
|
+
catch (_a) {
|
|
40
|
+
throw new Error("podman is installed but not responding — start the machine (macOS: 'podman machine start').");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function run(label, cmd, cwd) {
|
|
44
|
+
devkit_1.logger.info(`\n▸ ${label}`);
|
|
45
|
+
// Safe to echo: secrets are shell substitutions ($(gh auth token), $(grep …
|
|
46
|
+
// .env)), so the literal command never contains a resolved secret.
|
|
47
|
+
devkit_1.logger.info(` $ ${cmd}`);
|
|
48
|
+
(0, child_process_1.execSync)(cmd, { stdio: 'inherit', cwd, shell: '/bin/bash' });
|
|
49
|
+
}
|
|
50
|
+
// Run a command and capture stdout; returns '' on any failure.
|
|
51
|
+
function capture(cmd, cwd) {
|
|
52
|
+
try {
|
|
53
|
+
return (0, child_process_1.execSync)(cmd, { cwd, shell: '/bin/bash', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
54
|
+
.toString()
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// oc tag triggers an async imagestream reconcile, so a back-to-back
|
|
62
|
+
// import-image can 409 ("object has been modified"). Retry until it settles.
|
|
63
|
+
function importWithRetry(imageStreamTag, namespace, cwd, retries) {
|
|
64
|
+
for (let attempt = 1;; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
run(`Import image (attempt ${attempt}/${retries})`, `oc import-image ${imageStreamTag} --confirm -n ${namespace}`, cwd);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (attempt >= retries) {
|
|
71
|
+
throw new Error(`oc import-image failed after ${retries} attempts: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
devkit_1.logger.warn(` import-image failed (likely the oc tag reconcile race); retrying in 3s`);
|
|
74
|
+
(0, child_process_1.execSync)('sleep 3', { stdio: 'ignore' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function runExecutor(options, context) {
|
|
79
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
var _a, _b, _c, _d;
|
|
81
|
+
const projectName = context.projectName;
|
|
82
|
+
const project = projectName && ((_a = context.projectsConfigurations) === null || _a === void 0 ? void 0 : _a.projects[projectName]);
|
|
83
|
+
if (!projectName || !project) {
|
|
84
|
+
devkit_1.logger.error('[nx-oc:sandbox] Could not resolve the target project.');
|
|
85
|
+
return { success: false };
|
|
86
|
+
}
|
|
87
|
+
const { sandboxProject, registry, imageTag = 'sandbox', skipBuild = false, skipPush = false, deployBackend = false, importRetries = 5, } = options;
|
|
88
|
+
if (!sandboxProject) {
|
|
89
|
+
devkit_1.logger.error('[nx-oc:sandbox] "sandboxProject" is required.');
|
|
90
|
+
return { success: false };
|
|
91
|
+
}
|
|
92
|
+
if (!registry) {
|
|
93
|
+
devkit_1.logger.error('[nx-oc:sandbox] "registry" is required.');
|
|
94
|
+
return { success: false };
|
|
95
|
+
}
|
|
96
|
+
const appType = (_b = options.appType) !== null && _b !== void 0 ? _b : (0, app_type_1.detectApplicationType)(project);
|
|
97
|
+
if (!appType) {
|
|
98
|
+
devkit_1.logger.error(`[nx-oc:sandbox] Could not determine the application type for ${projectName}.`);
|
|
99
|
+
return { success: false };
|
|
100
|
+
}
|
|
101
|
+
const database = (_c = options.database) !== null && _c !== void 0 ? _c : 'none';
|
|
102
|
+
// Container registries (GHCR) require lowercase paths. Prefix the image with
|
|
103
|
+
// the (per-user) namespace so images from different experimenters never
|
|
104
|
+
// collide on GHCR's org-global package names.
|
|
105
|
+
const reg = registry.toLowerCase();
|
|
106
|
+
const registryHost = reg.split('/')[0];
|
|
107
|
+
const imageName = `${sandboxProject}-${projectName}`.toLowerCase();
|
|
108
|
+
const imageRef = `${reg}/${imageName}:${imageTag}`;
|
|
109
|
+
const projectRoot = project.root;
|
|
110
|
+
const cwd = context.root;
|
|
111
|
+
try {
|
|
112
|
+
// ---- preflight: check prerequisites before the expensive build ----
|
|
113
|
+
(0, oc_utils_1.ensureOcLogin)();
|
|
114
|
+
// The pull secret + import always need gh (session token to reach GHCR).
|
|
115
|
+
requireGhAuth();
|
|
116
|
+
if (!skipBuild || !skipPush) {
|
|
117
|
+
requirePodman();
|
|
118
|
+
}
|
|
119
|
+
// ---- service client secret (node services authenticate to ADSP) ----
|
|
120
|
+
// Upserted from the current .env so re-runs pick up a rotated secret.
|
|
121
|
+
if (appType === 'node') {
|
|
122
|
+
run('Upsert CLIENT_SECRET', `oc create secret generic ${projectName}-secrets ` +
|
|
123
|
+
`--from-literal=CLIENT_SECRET="$(grep -E '^CLIENT_SECRET=' ${projectRoot}/.env 2>/dev/null | cut -d= -f2-)" ` +
|
|
124
|
+
`-n ${sandboxProject} --dry-run=client -o yaml | oc apply -f -`, cwd);
|
|
125
|
+
}
|
|
126
|
+
// ---- shared database provisioning ----
|
|
127
|
+
if (database === 'postgres') {
|
|
128
|
+
run('Ensure Postgres credentials', `oc get secret sandbox-postgres-creds -n ${sandboxProject} 2>/dev/null || ` +
|
|
129
|
+
`oc create secret generic sandbox-postgres-creds ` +
|
|
130
|
+
`--from-literal=POSTGRESQL_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
|
|
131
|
+
`-n ${sandboxProject}`, cwd);
|
|
132
|
+
run('Deploy shared Postgres', `oc apply -f .openshift/sandbox/sandbox-postgres.yml -n ${sandboxProject}`, cwd);
|
|
133
|
+
// The shared Postgres only creates the admin database; each app needs its
|
|
134
|
+
// own <app>_sandbox database created before its migrate init container
|
|
135
|
+
// runs. Wait for Postgres, then create the database idempotently.
|
|
136
|
+
const dbName = `${projectName}_sandbox`;
|
|
137
|
+
run('Create app database', `oc rollout status deployment/sandbox-postgres -n ${sandboxProject} --timeout=180s && ` +
|
|
138
|
+
`oc exec -n ${sandboxProject} deployment/sandbox-postgres -- ` +
|
|
139
|
+
`bash -lc "psql -U postgres -tc \\"SELECT 1 FROM pg_database WHERE datname='${dbName}'\\" ` +
|
|
140
|
+
`| grep -q 1 || createdb -U postgres ${dbName}"`, cwd);
|
|
141
|
+
}
|
|
142
|
+
else if (database === 'mongo') {
|
|
143
|
+
run('Ensure MongoDB credentials', `oc get secret sandbox-mongodb-creds -n ${sandboxProject} 2>/dev/null || ` +
|
|
144
|
+
`oc create secret generic sandbox-mongodb-creds ` +
|
|
145
|
+
`--from-literal=MONGODB_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
|
|
146
|
+
`-n ${sandboxProject}`, cwd);
|
|
147
|
+
run('Deploy shared MongoDB', `oc apply -f .openshift/sandbox/sandbox-mongodb.yml -n ${sandboxProject}`, cwd);
|
|
148
|
+
}
|
|
149
|
+
// ---- paired backend Services (so a frontend's nginx resolves proxy_pass
|
|
150
|
+
// upstreams at startup). Only the Service is needed for DNS. Idempotent. ----
|
|
151
|
+
const proxyServices = ((_d = project.tags) !== null && _d !== void 0 ? _d : [])
|
|
152
|
+
.filter((tag) => tag.startsWith(PROXY_TAG_PREFIX))
|
|
153
|
+
.map((tag) => {
|
|
154
|
+
const value = tag.slice(PROXY_TAG_PREFIX.length);
|
|
155
|
+
const lastColon = value.lastIndexOf(':');
|
|
156
|
+
return { name: value.slice(0, lastColon), port: value.slice(lastColon + 1) };
|
|
157
|
+
});
|
|
158
|
+
for (const { name, port } of proxyServices) {
|
|
159
|
+
run(`Ensure paired Service ${name}`, `oc get service ${name} -n ${sandboxProject} >/dev/null 2>&1 || ` +
|
|
160
|
+
`oc create service clusterip ${name} --tcp=${port}:${port} -n ${sandboxProject}`, cwd);
|
|
161
|
+
}
|
|
162
|
+
// The Service stub above only stops nginx crashlooping; the backend needs
|
|
163
|
+
// its own pods for /api proxying to work. Deploy it (opt-in) or warn that
|
|
164
|
+
// proxied calls will 502 until it is deployed separately.
|
|
165
|
+
for (const { name } of proxyServices) {
|
|
166
|
+
if (deployBackend) {
|
|
167
|
+
try {
|
|
168
|
+
run(`Deploy paired backend ${name}`, `npx nx run ${name}:sandbox`, cwd);
|
|
169
|
+
}
|
|
170
|
+
catch (_e) {
|
|
171
|
+
throw new Error(`--deployBackend: could not deploy paired backend "${name}". Ensure it has a sandbox target ` +
|
|
172
|
+
`(nx g @abgov/nx-oc:sandbox ${name} --sandboxProject ${sandboxProject}).`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const endpoints = capture(`oc get endpoints ${name} -n ${sandboxProject} -o jsonpath='{.subsets[*].addresses[*].ip}'`, cwd);
|
|
177
|
+
if (!endpoints) {
|
|
178
|
+
devkit_1.logger.warn(`\n⚠ Paired backend "${name}" has no running pods in ${sandboxProject}. ` +
|
|
179
|
+
`The frontend will deploy, but requests proxied to ${name} will 502 until it is deployed:\n` +
|
|
180
|
+
` nx run ${name}:sandbox\n` +
|
|
181
|
+
` (or re-run this target with --deployBackend to deploy it first).`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ---- build locally + push to the registry ----
|
|
186
|
+
if (!skipBuild) {
|
|
187
|
+
run('Build', `npx nx build ${projectName} --configuration production`, cwd);
|
|
188
|
+
run('Podman build', `podman build --platform=linux/amd64 -f .openshift/${projectName}/Dockerfile -t ${imageRef} .`, cwd);
|
|
189
|
+
}
|
|
190
|
+
if (!skipPush) {
|
|
191
|
+
// gh supplies the token so no PAT is stored; the same session token backs
|
|
192
|
+
// the pull secret below.
|
|
193
|
+
run('Registry login', `gh auth token | podman login ${registryHost} -u "$(gh api user -q .login)" --password-stdin`, cwd);
|
|
194
|
+
run('Push image', `podman push ${imageRef}`, cwd);
|
|
195
|
+
}
|
|
196
|
+
// ---- import into the namespace imagestream + roll out ----
|
|
197
|
+
// Per-deploy pull secret from the gh session (no long-lived PAT needed).
|
|
198
|
+
run('Upsert pull secret', `oc create secret docker-registry ghcr-pull ` +
|
|
199
|
+
`--docker-server=${registryHost} --docker-username="$(gh api user -q .login)" --docker-password="$(gh auth token)" ` +
|
|
200
|
+
`-n ${sandboxProject} --dry-run=client -o yaml | oc apply -f -`, cwd);
|
|
201
|
+
// oc tag sets/repoints the imagestream tag with reference-policy=local so
|
|
202
|
+
// pods pull in-cluster; import --confirm then pulls the current manifest.
|
|
203
|
+
run('Tag imagestream', `oc tag ${imageRef} ${projectName}:${imageTag} --reference-policy=local -n ${sandboxProject}`, cwd);
|
|
204
|
+
importWithRetry(`${projectName}:${imageTag}`, sandboxProject, cwd, importRetries);
|
|
205
|
+
run('Apply manifest', `oc process -f .openshift/${projectName}/${projectName}.yml -p PROJECT=${sandboxProject} | oc apply -f -`, cwd);
|
|
206
|
+
run('Restart rollout', `oc rollout restart deployment/${projectName} -n ${sandboxProject}`, cwd);
|
|
207
|
+
run('Wait for rollout', `oc rollout status deployment/${projectName} -n ${sandboxProject} --timeout=180s`, cwd);
|
|
208
|
+
devkit_1.logger.info(`\n✓ Sandbox deploy complete for ${projectName}.`);
|
|
209
|
+
return { success: true };
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
devkit_1.logger.error(`\n[nx-oc:sandbox] ${err.message}`);
|
|
213
|
+
return { success: false };
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/executors/sandbox/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { ExecutorContext, logger } from '@nx/devkit';
|
|
2
|
+
import runExecutor from './sandbox';
|
|
3
|
+
import { SandboxExecutorSchema } from './schema';
|
|
4
|
+
|
|
5
|
+
jest.mock('child_process', () => ({
|
|
6
|
+
...jest.requireActual('child_process'),
|
|
7
|
+
execSync: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
jest.mock('../../utils/oc-utils', () => ({ ensureOcLogin: jest.fn() }));
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
|
+
const { execSync } = require('child_process') as { execSync: jest.Mock };
|
|
13
|
+
|
|
14
|
+
const IMAGE_REF = 'ghcr.io/test-org/test-sandbox-test:sandbox';
|
|
15
|
+
|
|
16
|
+
function context(tags: string[] = []): ExecutorContext {
|
|
17
|
+
return {
|
|
18
|
+
root: '/ws',
|
|
19
|
+
cwd: '/ws',
|
|
20
|
+
isVerbose: false,
|
|
21
|
+
projectName: 'test',
|
|
22
|
+
projectsConfigurations: {
|
|
23
|
+
version: 2,
|
|
24
|
+
projects: {
|
|
25
|
+
test: { root: 'apps/test', tags },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
} as unknown as ExecutorContext;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const baseOptions: SandboxExecutorSchema = {
|
|
32
|
+
sandboxProject: 'test-sandbox',
|
|
33
|
+
registry: 'ghcr.io/test-org',
|
|
34
|
+
appType: 'node',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// The commands passed to execSync, in call order.
|
|
38
|
+
function commands(): string[] {
|
|
39
|
+
return execSync.mock.calls.map((c) => c[0] as string);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
execSync.mockReset();
|
|
44
|
+
execSync.mockImplementation(() => Buffer.from(''));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('sandbox executor', () => {
|
|
48
|
+
it('builds, pushes, tags, imports, and rolls out in order', async () => {
|
|
49
|
+
const result = await runExecutor(baseOptions, context());
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
|
|
52
|
+
const cmds = commands();
|
|
53
|
+
const has = (s: string) => cmds.some((c) => c.includes(s));
|
|
54
|
+
expect(has('nx build test --configuration production')).toBe(true);
|
|
55
|
+
expect(
|
|
56
|
+
has(
|
|
57
|
+
`podman build --platform=linux/amd64 -f .openshift/test/Dockerfile -t ${IMAGE_REF} .`
|
|
58
|
+
)
|
|
59
|
+
).toBe(true);
|
|
60
|
+
expect(has(`podman push ${IMAGE_REF}`)).toBe(true);
|
|
61
|
+
expect(
|
|
62
|
+
has(`oc tag ${IMAGE_REF} test:sandbox --reference-policy=local -n test-sandbox`)
|
|
63
|
+
).toBe(true);
|
|
64
|
+
expect(has('oc import-image test:sandbox --confirm -n test-sandbox')).toBe(true);
|
|
65
|
+
expect(has('oc create secret docker-registry ghcr-pull')).toBe(true);
|
|
66
|
+
expect(has('oc rollout restart deployment/test -n test-sandbox')).toBe(true);
|
|
67
|
+
expect(has('oc rollout status deployment/test -n test-sandbox')).toBe(true);
|
|
68
|
+
|
|
69
|
+
// ordering: build precedes push precedes tag precedes import precedes rollout
|
|
70
|
+
const idx = (s: string) => cmds.findIndex((c) => c.includes(s));
|
|
71
|
+
expect(idx('podman build')).toBeLessThan(idx('podman push'));
|
|
72
|
+
expect(idx('podman push')).toBeLessThan(idx('oc tag'));
|
|
73
|
+
expect(idx('oc tag')).toBeLessThan(idx('oc import-image'));
|
|
74
|
+
expect(idx('oc import-image')).toBeLessThan(idx('oc rollout restart'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('mirrors CLIENT_SECRET for a node service only', async () => {
|
|
78
|
+
await runExecutor(baseOptions, context());
|
|
79
|
+
expect(
|
|
80
|
+
commands().some(
|
|
81
|
+
(c) => c.includes('oc create secret generic test-secrets') && c.includes('CLIENT_SECRET')
|
|
82
|
+
)
|
|
83
|
+
).toBe(true);
|
|
84
|
+
|
|
85
|
+
execSync.mockClear();
|
|
86
|
+
await runExecutor({ ...baseOptions, appType: 'frontend' }, context());
|
|
87
|
+
expect(commands().some((c) => c.includes('test-secrets'))).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('fails fast with an actionable message when podman is missing', async () => {
|
|
91
|
+
execSync.mockImplementation((cmd: string) => {
|
|
92
|
+
if (cmd.startsWith('command -v podman')) throw new Error('not found');
|
|
93
|
+
return Buffer.from('');
|
|
94
|
+
});
|
|
95
|
+
const result = await runExecutor(baseOptions, context());
|
|
96
|
+
expect(result.success).toBe(false);
|
|
97
|
+
// did not proceed to the build
|
|
98
|
+
expect(commands().some((c) => c.includes('podman build'))).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('fails fast when the podman machine is not running', async () => {
|
|
102
|
+
execSync.mockImplementation((cmd: string) => {
|
|
103
|
+
if (cmd === 'podman info') throw new Error('cannot connect');
|
|
104
|
+
return Buffer.from('');
|
|
105
|
+
});
|
|
106
|
+
const result = await runExecutor(baseOptions, context());
|
|
107
|
+
expect(result.success).toBe(false);
|
|
108
|
+
expect(commands().some((c) => c.includes('podman build'))).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('fails fast when gh is installed but not authenticated (before the build)', async () => {
|
|
112
|
+
execSync.mockImplementation((cmd: string) => {
|
|
113
|
+
if (cmd === 'gh auth status') throw new Error('not logged in');
|
|
114
|
+
return Buffer.from('');
|
|
115
|
+
});
|
|
116
|
+
const result = await runExecutor(baseOptions, context());
|
|
117
|
+
expect(result.success).toBe(false);
|
|
118
|
+
// gh checked up front — no build/push happened
|
|
119
|
+
expect(commands().some((c) => c.includes('nx build test'))).toBe(false);
|
|
120
|
+
expect(commands().some((c) => c.includes('podman build'))).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('retries oc import-image on the tag-reconcile race', async () => {
|
|
124
|
+
let importAttempts = 0;
|
|
125
|
+
execSync.mockImplementation((cmd: string) => {
|
|
126
|
+
if (cmd.includes('oc import-image')) {
|
|
127
|
+
importAttempts++;
|
|
128
|
+
if (importAttempts < 3) throw new Error('409 the object has been modified');
|
|
129
|
+
}
|
|
130
|
+
return Buffer.from('');
|
|
131
|
+
});
|
|
132
|
+
const result = await runExecutor(baseOptions, context());
|
|
133
|
+
expect(result.success).toBe(true);
|
|
134
|
+
expect(importAttempts).toBe(3);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('gives up after importRetries and fails', async () => {
|
|
138
|
+
execSync.mockImplementation((cmd: string) => {
|
|
139
|
+
if (cmd.includes('oc import-image')) throw new Error('409');
|
|
140
|
+
return Buffer.from('');
|
|
141
|
+
});
|
|
142
|
+
const result = await runExecutor({ ...baseOptions, importRetries: 2 }, context());
|
|
143
|
+
expect(result.success).toBe(false);
|
|
144
|
+
const importCalls = commands().filter((c) => c.includes('oc import-image'));
|
|
145
|
+
expect(importCalls.length).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('skipBuild/skipPush reuse the existing image', async () => {
|
|
149
|
+
await runExecutor({ ...baseOptions, skipBuild: true, skipPush: true }, context());
|
|
150
|
+
const cmds = commands();
|
|
151
|
+
expect(cmds.some((c) => c.includes('podman build'))).toBe(false);
|
|
152
|
+
expect(cmds.some((c) => c.includes('nx build test'))).toBe(false);
|
|
153
|
+
expect(cmds.some((c) => c.includes('podman push'))).toBe(false);
|
|
154
|
+
// still imports + rolls out
|
|
155
|
+
expect(cmds.some((c) => c.includes('oc import-image'))).toBe(true);
|
|
156
|
+
expect(cmds.some((c) => c.includes('oc rollout restart'))).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('provisions Postgres and the per-app database before rollout', async () => {
|
|
160
|
+
await runExecutor({ ...baseOptions, database: 'postgres' }, context());
|
|
161
|
+
const cmds = commands();
|
|
162
|
+
expect(cmds.some((c) => c.includes('sandbox-postgres-creds'))).toBe(true);
|
|
163
|
+
expect(cmds.some((c) => c.includes('sandbox-postgres.yml'))).toBe(true);
|
|
164
|
+
expect(cmds.some((c) => c.includes('createdb -U postgres test_sandbox'))).toBe(true);
|
|
165
|
+
const createDbIdx = cmds.findIndex((c) => c.includes('createdb'));
|
|
166
|
+
const rolloutIdx = cmds.findIndex((c) => c.includes('rollout status deployment/test'));
|
|
167
|
+
expect(createDbIdx).toBeGreaterThanOrEqual(0);
|
|
168
|
+
expect(createDbIdx).toBeLessThan(rolloutIdx);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('ensures paired backend Services from proxy-service tags (idempotent)', async () => {
|
|
172
|
+
await runExecutor(
|
|
173
|
+
{ ...baseOptions, appType: 'frontend' },
|
|
174
|
+
context(['adsp:proxy-service:test-service:3333'])
|
|
175
|
+
);
|
|
176
|
+
const guard = commands().find((c) => c.includes('oc get service test-service'));
|
|
177
|
+
expect(guard).toBeTruthy();
|
|
178
|
+
expect(guard).toContain('||');
|
|
179
|
+
expect(guard).toContain('oc create service clusterip test-service --tcp=3333:3333');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('adds no paired-service guard without proxy-service tags', async () => {
|
|
183
|
+
await runExecutor(baseOptions, context());
|
|
184
|
+
expect(commands().some((c) => c.includes('oc get service'))).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('warns when a paired backend has no running pods', async () => {
|
|
188
|
+
const warn = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
189
|
+
// default mock returns '' for `oc get endpoints` → no endpoints
|
|
190
|
+
await runExecutor(
|
|
191
|
+
{ ...baseOptions, appType: 'frontend' },
|
|
192
|
+
context(['adsp:proxy-service:test-service:3333'])
|
|
193
|
+
);
|
|
194
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('test-service'));
|
|
195
|
+
// warn only — does not deploy the backend
|
|
196
|
+
expect(commands().some((c) => c.includes('nx run test-service:sandbox'))).toBe(false);
|
|
197
|
+
warn.mockRestore();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('does not warn when the paired backend has endpoints', async () => {
|
|
201
|
+
const warn = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
202
|
+
execSync.mockImplementation((cmd: string) =>
|
|
203
|
+
Buffer.from(cmd.includes('oc get endpoints test-service') ? '10.1.2.3' : '')
|
|
204
|
+
);
|
|
205
|
+
await runExecutor(
|
|
206
|
+
{ ...baseOptions, appType: 'frontend' },
|
|
207
|
+
context(['adsp:proxy-service:test-service:3333'])
|
|
208
|
+
);
|
|
209
|
+
expect(warn).not.toHaveBeenCalled();
|
|
210
|
+
warn.mockRestore();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('deployBackend deploys each paired backend first (no warning)', async () => {
|
|
214
|
+
const warn = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
215
|
+
await runExecutor(
|
|
216
|
+
{ ...baseOptions, appType: 'frontend', deployBackend: true },
|
|
217
|
+
context(['adsp:proxy-service:test-service:3333'])
|
|
218
|
+
);
|
|
219
|
+
expect(commands().some((c) => c.includes('npx nx run test-service:sandbox'))).toBe(true);
|
|
220
|
+
expect(warn).not.toHaveBeenCalled();
|
|
221
|
+
warn.mockRestore();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ApplicationType, DatabaseType } from '../../generators/deployment/schema';
|
|
2
|
+
|
|
3
|
+
export interface SandboxExecutorSchema {
|
|
4
|
+
sandboxProject: string;
|
|
5
|
+
registry: string;
|
|
6
|
+
database?: DatabaseType;
|
|
7
|
+
appType?: ApplicationType;
|
|
8
|
+
imageTag?: string;
|
|
9
|
+
skipBuild?: boolean;
|
|
10
|
+
skipPush?: boolean;
|
|
11
|
+
deployBackend?: boolean;
|
|
12
|
+
importRetries?: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"outputCapture": "direct-nodejs",
|
|
4
|
+
"$schema": "http://json-schema.org/schema",
|
|
5
|
+
"title": "Sandbox deploy executor",
|
|
6
|
+
"description": "Builds the project image locally with podman, pushes it to the container registry, imports it into the sandbox namespace, and rolls out the deployment.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"sandboxProject": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The OpenShift namespace to deploy the sandbox into."
|
|
12
|
+
},
|
|
13
|
+
"registry": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Container registry the sandbox image is published to (e.g. ghcr.io/my-org)."
|
|
16
|
+
},
|
|
17
|
+
"database": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["none", "postgres", "mongo"],
|
|
20
|
+
"default": "none",
|
|
21
|
+
"description": "Provision a shared sandbox database of this type before deploying."
|
|
22
|
+
},
|
|
23
|
+
"appType": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["node", "frontend", "dotnet"],
|
|
26
|
+
"description": "Application type. Detected from the project configuration when omitted."
|
|
27
|
+
},
|
|
28
|
+
"imageTag": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"default": "sandbox",
|
|
31
|
+
"description": "Image tag pushed and imported into the namespace imagestream."
|
|
32
|
+
},
|
|
33
|
+
"skipBuild": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"default": false,
|
|
36
|
+
"description": "Skip the nx build + podman build steps and reuse the already-built image (resume a partial deploy)."
|
|
37
|
+
},
|
|
38
|
+
"skipPush": {
|
|
39
|
+
"type": "boolean",
|
|
40
|
+
"default": false,
|
|
41
|
+
"description": "Skip the registry login + push and reuse the already-pushed image (resume from the import step)."
|
|
42
|
+
},
|
|
43
|
+
"deployBackend": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"default": false,
|
|
46
|
+
"description": "For a frontend: deploy each paired backend (its own sandbox target) first, so proxied /api calls work. Off by default; without it, a missing backend is warned about, not deployed."
|
|
47
|
+
},
|
|
48
|
+
"importRetries": {
|
|
49
|
+
"type": "number",
|
|
50
|
+
"default": 5,
|
|
51
|
+
"description": "How many times to retry oc import-image (absorbs the 409 from oc tag's async reconcile)."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": ["sandboxProject", "registry"]
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Sandbox deploy — <%= projectName %>
|
|
2
|
+
|
|
3
|
+
Deploy runbook for coding agents. `nx run <%= projectName %>:sandbox` builds the
|
|
4
|
+
image **locally with podman**, pushes it to GHCR, imports it into the
|
|
5
|
+
`<%= sandboxProject %>` namespace, and rolls out — no git push or CI wait. The
|
|
6
|
+
orchestration lives in the `@abgov/nx-oc:sandbox` executor (versioned in the
|
|
7
|
+
plugin), so the steps below are what it runs for you.
|
|
8
|
+
|
|
9
|
+
## What the target does, in order
|
|
10
|
+
|
|
11
|
+
1. **Preflight** — `oc` login, `gh` auth, and `podman` machine are checked up
|
|
12
|
+
front; a failure here stops before the build with an actionable message.
|
|
13
|
+
2. Upsert the pull secret + <% if (appType === 'node') { %>the `CLIENT_SECRET` secret + <% } %>any paired Services<% if (database && database !== 'none') { %> + the shared `<%= database %>` database<% } %>.
|
|
14
|
+
3. `nx build <%= projectName %> --configuration production`
|
|
15
|
+
4. `podman build` → `podman login` (gh token) → `podman push` to
|
|
16
|
+
`<%= registry %>/<%= imageName %>:sandbox`.
|
|
17
|
+
5. `oc tag … --reference-policy=local` then `oc import-image` (retried to absorb
|
|
18
|
+
the tag-reconcile 409).
|
|
19
|
+
6. `oc process | oc apply` the manifest, then `oc rollout restart` + `status`.
|
|
20
|
+
|
|
21
|
+
## Options (resume a partial deploy without rebuilding)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
nx run <%= projectName %>:sandbox --skipBuild # reuse the built image; re-push + import + roll out
|
|
25
|
+
nx run <%= projectName %>:sandbox --skipBuild --skipPush # reuse the pushed image; re-import + roll out only
|
|
26
|
+
nx run <%= projectName %>:sandbox --imageTag=<tag> # push/import under a different tag
|
|
27
|
+
nx run <%= projectName %>:sandbox --importRetries=8 # more import retries on a slow cluster
|
|
28
|
+
<% if (appType === 'frontend') { %>nx run <%= projectName %>:sandbox --deployBackend # deploy each paired backend (its own sandbox) first
|
|
29
|
+
<% } %>```
|
|
30
|
+
<% if (appType === 'frontend') { %>
|
|
31
|
+
This frontend proxies `/api` to its paired backend Service. The target always
|
|
32
|
+
ensures that Service exists (so nginx doesn't crashloop), but by default it does
|
|
33
|
+
**not** deploy the backend — if the backend has no running pods it **warns** that
|
|
34
|
+
proxied calls will 502, and names the target to run. Pass `--deployBackend` to
|
|
35
|
+
deploy each paired backend as part of this run instead.
|
|
36
|
+
<% } %>
|
|
37
|
+
|
|
38
|
+
## Preflight failures — cause → fix
|
|
39
|
+
|
|
40
|
+
- **`'podman' is required …`** — podman not installed. `brew install podman` (macOS).
|
|
41
|
+
- **`podman is installed but not responding`** — machine stopped. `podman machine start`.
|
|
42
|
+
- **`'gh' is required …`** — GitHub CLI not installed. See https://cli.github.com.
|
|
43
|
+
- **`gh is installed but not authenticated`** — run `gh auth login`.
|
|
44
|
+
- **oc login prompts / fails** — `oc login` to your cluster, targeting the
|
|
45
|
+
`<%= sandboxProject %>` namespace (`oc project <%= sandboxProject %>`).
|
|
46
|
+
|
|
47
|
+
## If the deploy fails mid-way (work around by hand)
|
|
48
|
+
|
|
49
|
+
The executor stops at the first failing step. After fixing the cause, the
|
|
50
|
+
fastest path is usually `--skipBuild` (or `--skipBuild --skipPush`) to resume.
|
|
51
|
+
If you need to drive it manually, these are the remaining steps — the image ref
|
|
52
|
+
is `<%= registry %>/<%= imageName %>:sandbox`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
NS=<%= sandboxProject %>
|
|
56
|
+
REF=<%= registry %>/<%= imageName %>:sandbox
|
|
57
|
+
# (1) push, if not already pushed
|
|
58
|
+
gh auth token | podman login <%= registryHost %> -u "$(gh api user -q .login)" --password-stdin
|
|
59
|
+
podman push "$REF"
|
|
60
|
+
# (2) point the imagestream at it and import (retry absorbs the tag race)
|
|
61
|
+
oc tag "$REF" <%= projectName %>:sandbox --reference-policy=local -n "$NS"
|
|
62
|
+
until oc import-image <%= projectName %>:sandbox --confirm -n "$NS"; do sleep 3; done
|
|
63
|
+
# (3) apply the manifest and roll out
|
|
64
|
+
oc process -f .openshift/<%= projectName %>/<%= projectName %>.yml -p PROJECT="$NS" | oc apply -f -
|
|
65
|
+
oc rollout restart deployment/<%= projectName %> -n "$NS"
|
|
66
|
+
oc rollout status deployment/<%= projectName %> -n "$NS" --timeout=180s
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Verify
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
oc get pods -n <%= sandboxProject %> -l name=<%= projectName %>
|
|
73
|
+
<% if (appType === 'node') { %>curl "https://$(oc get route <%= projectName %> -n <%= sandboxProject %> -o jsonpath='{.spec.host}')/health"<% } else { %>curl -I "https://$(oc get route <%= projectName %> -n <%= sandboxProject %> -o jsonpath='{.spec.host}')/"<% } %>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Known issues & workarounds
|
|
77
|
+
|
|
78
|
+
- **`podman push` / import fails with an auth error** — the *active* `gh`
|
|
79
|
+
account lacks `write:packages` (or access to the org). Preflight only confirms
|
|
80
|
+
*an* account is logged in, not its scope. Check `gh auth status`; switch with
|
|
81
|
+
`gh auth switch -u <account>`, then re-run with `--skipBuild`.
|
|
82
|
+
- **Pod `FailedCreate` "exceeded quota"** — the namespace CPU quota is full.
|
|
83
|
+
Free room: `oc get deploy -n <%= sandboxProject %>` then delete stale apps
|
|
84
|
+
(`oc delete all,configmap,is -l app=<old-app> -n <%= sandboxProject %>`), and re-run.
|
|
85
|
+
- **Pod `CrashLoopBackOff`** — `oc logs -n <%= sandboxProject %> <pod> -c <%= projectName %>`.<% if (appType === 'node' && database && database !== 'none') { %>
|
|
86
|
+
For the DB migration, also check the init container:
|
|
87
|
+
`oc logs -n <%= sandboxProject %> <pod> -c <%= projectName %>-migrate`.<% } %>
|
|
88
|
+
<% if (appType === 'frontend') { %>- **Sign-in returns `Invalid redirect_uri`** — the sandbox Route wasn't
|
|
89
|
+
registered on the Keycloak public client (the generator does this when it can
|
|
90
|
+
resolve the cluster ingress domain). Add `https://<%= projectName %>-<%= sandboxProject %>.<ingress-domain>/*`
|
|
91
|
+
to the client's valid redirect URIs.
|
|
92
|
+
<% } %>- **Import 409 "object has been modified"** — the `oc tag` reconcile race; the
|
|
93
|
+
executor already retries. If you hit it manually, just re-run the import.
|
|
94
|
+
|
|
95
|
+
## Teardown
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
nx run <%= projectName %>:sandbox-teardown # remove sandbox resources + delete the GHCR image
|
|
99
|
+
```
|
|
@@ -70,103 +70,39 @@ function addManifestFiles(host, options) {
|
|
|
70
70
|
sourceRepositoryUrl: (_a = options.sourceRepositoryUrl) !== null && _a !== void 0 ? _a : '', database: (_b = options.database) !== null && _b !== void 0 ? _b : 'none', sandbox: true, ocInfraProject: options.sandboxProject, tmpl: '' });
|
|
71
71
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, `../deployment/${options.appType}-files`), `./.openshift/${options.projectName}`, templateOptions);
|
|
72
72
|
}
|
|
73
|
+
// Emit the per-app deploy + troubleshooting runbook next to the manifests, so a
|
|
74
|
+
// coding agent has one canonical, app-aware source (rather than the guidance
|
|
75
|
+
// being duplicated across every app's AGENTS.md).
|
|
76
|
+
function addSandboxDoc(host, options) {
|
|
77
|
+
var _a;
|
|
78
|
+
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), `./.openshift/${options.projectName}`, {
|
|
79
|
+
projectName: options.projectName,
|
|
80
|
+
appType: options.appType,
|
|
81
|
+
database: (_a = options.database) !== null && _a !== void 0 ? _a : 'none',
|
|
82
|
+
sandboxProject: options.sandboxProject,
|
|
83
|
+
registry: options.registry,
|
|
84
|
+
registryHost: options.registryHost,
|
|
85
|
+
imageName: options.imageName,
|
|
86
|
+
tmpl: '',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
73
89
|
function addDatabaseFiles(host, options) {
|
|
74
90
|
if (!options.database || options.database === 'none')
|
|
75
91
|
return;
|
|
76
92
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'database-files'), './.openshift/sandbox', { database: options.database, tmpl: '' });
|
|
77
93
|
}
|
|
78
94
|
function addSandboxTarget(host, options) {
|
|
79
|
-
var _a;
|
|
80
95
|
const config = (0, devkit_1.readProjectConfiguration)(host, options.project);
|
|
81
|
-
const { projectName, sandboxProject, database, appType,
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// OpenShift Secret the deployment reads. Upserted from the current .env so
|
|
87
|
-
// re-runs pick up a rotated secret. Non-node app types have no client secret.
|
|
88
|
-
if (appType === 'node') {
|
|
89
|
-
commands.push(`oc create secret generic ${projectName}-secrets ` +
|
|
90
|
-
`--from-literal=CLIENT_SECRET="$(grep -E '^CLIENT_SECRET=' ${config.root}/.env 2>/dev/null | cut -d= -f2-)" ` +
|
|
91
|
-
`-n ${sandboxProject} --dry-run=client -o yaml | oc apply -f -`);
|
|
92
|
-
}
|
|
93
|
-
if (database === 'postgres') {
|
|
94
|
-
commands.push(`oc get secret sandbox-postgres-creds -n ${sandboxProject} 2>/dev/null || ` +
|
|
95
|
-
`oc create secret generic sandbox-postgres-creds ` +
|
|
96
|
-
`--from-literal=POSTGRESQL_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
|
|
97
|
-
`-n ${sandboxProject}`);
|
|
98
|
-
commands.push(`oc apply -f .openshift/sandbox/sandbox-postgres.yml -n ${sandboxProject}`);
|
|
99
|
-
// The shared Postgres instance only creates the admin database; each app
|
|
100
|
-
// needs its own <app>_sandbox database created before its migrate init
|
|
101
|
-
// container runs (neither the image nor the ORM creates it). Wait for
|
|
102
|
-
// Postgres to be ready, then create the database idempotently.
|
|
103
|
-
const dbName = `${projectName}_sandbox`;
|
|
104
|
-
commands.push(`oc rollout status deployment/sandbox-postgres -n ${sandboxProject} --timeout=180s && ` +
|
|
105
|
-
`oc exec -n ${sandboxProject} deployment/sandbox-postgres -- ` +
|
|
106
|
-
`bash -lc "psql -U postgres -tc \\"SELECT 1 FROM pg_database WHERE datname='${dbName}'\\" ` +
|
|
107
|
-
`| grep -q 1 || createdb -U postgres ${dbName}"`);
|
|
108
|
-
}
|
|
109
|
-
else if (database === 'mongo') {
|
|
110
|
-
commands.push(`oc get secret sandbox-mongodb-creds -n ${sandboxProject} 2>/dev/null || ` +
|
|
111
|
-
`oc create secret generic sandbox-mongodb-creds ` +
|
|
112
|
-
`--from-literal=MONGODB_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
|
|
113
|
-
`-n ${sandboxProject}`);
|
|
114
|
-
commands.push(`oc apply -f .openshift/sandbox/sandbox-mongodb.yml -n ${sandboxProject}`);
|
|
115
|
-
}
|
|
116
|
-
// Ensure any paired backend Services exist first, so this app's nginx can
|
|
117
|
-
// resolve its proxy_pass upstreams at startup (otherwise the pod crashloops
|
|
118
|
-
// until the Service appears). Create only the Service (all nginx needs for
|
|
119
|
-
// DNS) — not the backend's deployment, which has no image until its own
|
|
120
|
-
// sandbox runs. Idempotent: skipped once the Service exists, so it doesn't
|
|
121
|
-
// slow down repeated frontend iteration. Recorded by the composite generators
|
|
122
|
-
// (pevn/mevn/…) as `adsp:proxy-service:<name>:<port>` project tags.
|
|
123
|
-
const PREFIX = 'adsp:proxy-service:';
|
|
124
|
-
const proxyServices = ((_a = config.tags) !== null && _a !== void 0 ? _a : [])
|
|
125
|
-
.filter((tag) => tag.startsWith(PREFIX))
|
|
126
|
-
.map((tag) => {
|
|
127
|
-
const value = tag.slice(PREFIX.length);
|
|
128
|
-
const lastColon = value.lastIndexOf(':');
|
|
129
|
-
return {
|
|
130
|
-
name: value.slice(0, lastColon),
|
|
131
|
-
port: value.slice(lastColon + 1),
|
|
132
|
-
};
|
|
133
|
-
});
|
|
134
|
-
for (const { name, port } of proxyServices) {
|
|
135
|
-
commands.push(`oc get service ${name} -n ${sandboxProject} >/dev/null 2>&1 || ` +
|
|
136
|
-
`oc create service clusterip ${name} --tcp=${port}:${port} -n ${sandboxProject}`);
|
|
137
|
-
}
|
|
138
|
-
// Build the image locally and push to the container registry, then import it
|
|
139
|
-
// into the namespace's imagestream. reference-policy=local mirrors it into the
|
|
140
|
-
// internal registry so pods pull in-cluster (no per-pod pull secret, no node
|
|
141
|
-
// egress to the registry). This replaces the in-cluster BuildConfig: no
|
|
142
|
-
// full-workspace upload, and local layer caching makes iteration fast.
|
|
143
|
-
commands.push(`npx nx build ${projectName} --configuration production`);
|
|
144
|
-
commands.push(`podman build --platform=linux/amd64 -f .openshift/${projectName}/Dockerfile -t ${imageRef} .`);
|
|
145
|
-
// Prereq: the publishing account is logged in with write:packages. gh supplies
|
|
146
|
-
// the token so no PAT is stored; the same session token backs the pull secret.
|
|
147
|
-
commands.push(`gh auth token | podman login ${registryHost} -u "$(gh api user -q .login)" --password-stdin`);
|
|
148
|
-
commands.push(`podman push ${imageRef}`);
|
|
149
|
-
// Per-deploy pull secret from the gh session (sandbox images are re-imported
|
|
150
|
-
// every run, so a session token is sufficient — no long-lived PAT needed).
|
|
151
|
-
commands.push(`oc create secret docker-registry ghcr-pull ` +
|
|
152
|
-
`--docker-server=${registryHost} --docker-username="$(gh api user -q .login)" --docker-password="$(gh auth token)" ` +
|
|
153
|
-
`-n ${sandboxProject} --dry-run=client -o yaml | oc apply -f -`);
|
|
154
|
-
// oc tag sets/repoints the imagestream tag (import-image refuses to change an
|
|
155
|
-
// existing tag's source); import --confirm then pulls the manifest.
|
|
156
|
-
commands.push(`oc tag ${imageRef} ${projectName}:sandbox --reference-policy=local -n ${sandboxProject}`);
|
|
157
|
-
// oc tag triggers an async imagestream reconcile, so a back-to-back
|
|
158
|
-
// import-image can 409 ("object has been modified"). Retry until it settles.
|
|
159
|
-
commands.push(`n=0; until oc import-image ${projectName}:sandbox --confirm -n ${sandboxProject}; do ` +
|
|
160
|
-
`n=$((n+1)); [ $n -ge 5 ] && exit 1; sleep 3; done`);
|
|
161
|
-
commands.push(`oc process -f .openshift/${projectName}/${projectName}.yml -p PROJECT=${sandboxProject} | oc apply -f -`);
|
|
162
|
-
commands.push(`oc rollout restart deployment/${projectName} -n ${sandboxProject}`);
|
|
163
|
-
commands.push(`oc rollout status deployment/${projectName} -n ${sandboxProject} --timeout=180s`);
|
|
96
|
+
const { projectName, sandboxProject, database, appType, registry, registryOrg, imageName, } = options;
|
|
97
|
+
// The deploy orchestration (preflight, build → podman → push → import with
|
|
98
|
+
// retry → rollout, database/paired-service provisioning) lives in the
|
|
99
|
+
// @abgov/nx-oc:sandbox executor, versioned in the plugin — so bug fixes reach
|
|
100
|
+
// every project on `npm update` instead of being baked into project.json.
|
|
164
101
|
config.targets = Object.assign(Object.assign({}, config.targets), { sandbox: {
|
|
165
|
-
executor: 'nx:
|
|
166
|
-
options: {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
},
|
|
102
|
+
executor: '@abgov/nx-oc:sandbox',
|
|
103
|
+
options: Object.assign({ sandboxProject,
|
|
104
|
+
registry,
|
|
105
|
+
appType }, (database && database !== 'none' ? { database } : {})),
|
|
170
106
|
}, 'sandbox-teardown': {
|
|
171
107
|
executor: 'nx:run-commands',
|
|
172
108
|
options: {
|
|
@@ -214,6 +150,7 @@ function default_1(host, options) {
|
|
|
214
150
|
}
|
|
215
151
|
addManifestFiles(host, normalizedOptions);
|
|
216
152
|
addDatabaseFiles(host, normalizedOptions);
|
|
153
|
+
addSandboxDoc(host, normalizedOptions);
|
|
217
154
|
addSandboxTarget(host, normalizedOptions);
|
|
218
155
|
yield registerSandboxRedirectUri(normalizedOptions);
|
|
219
156
|
yield (0, devkit_1.formatFiles)(host);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/sandbox/sandbox.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/sandbox/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -83,64 +83,61 @@ describe('Sandbox Generator', () => {
|
|
|
83
83
|
expect(manifest).not.toContain('ImageStream');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
it('adds sandbox nx
|
|
86
|
+
it('adds a sandbox target wired to the @abgov/nx-oc:sandbox executor', async () => {
|
|
87
87
|
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
88
88
|
addNodeProject(host);
|
|
89
89
|
|
|
90
90
|
await generator(host, options);
|
|
91
91
|
|
|
92
92
|
const config = readProjectConfiguration(host, 'test');
|
|
93
|
-
|
|
94
|
-
expect(
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
expect(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const imageRef = 'ghcr.io/test-org/test-sandbox-test:sandbox';
|
|
106
|
-
expect(
|
|
107
|
-
cmds.some((c) => c.includes('podman build') && c.includes(imageRef))
|
|
108
|
-
).toBeTruthy();
|
|
109
|
-
expect(cmds.some((c) => c.includes(`podman push ${imageRef}`))).toBeTruthy();
|
|
110
|
-
expect(cmds.some((c) => c.includes('podman login ghcr.io'))).toBeTruthy();
|
|
111
|
-
|
|
112
|
-
// Import into the namespace imagestream; pods pull from the internal registry.
|
|
113
|
-
expect(
|
|
114
|
-
cmds.some((c) =>
|
|
115
|
-
c.includes(`oc tag ${imageRef} test:sandbox --reference-policy=local`)
|
|
116
|
-
)
|
|
117
|
-
).toBeTruthy();
|
|
118
|
-
// import-image is retried to absorb the 409 from oc tag's async reconcile.
|
|
119
|
-
expect(
|
|
120
|
-
cmds.some(
|
|
121
|
-
(c) =>
|
|
122
|
-
c.includes('oc import-image test:sandbox --confirm') &&
|
|
123
|
-
c.includes('until')
|
|
124
|
-
)
|
|
125
|
-
).toBeTruthy();
|
|
126
|
-
// Per-deploy pull secret from the gh session (no stored PAT).
|
|
127
|
-
expect(
|
|
128
|
-
cmds.some((c) => c.includes('oc create secret docker-registry ghcr-pull'))
|
|
129
|
-
).toBeTruthy();
|
|
93
|
+
const target = config.targets['sandbox'];
|
|
94
|
+
expect(target).toBeTruthy();
|
|
95
|
+
// Deploy orchestration lives in the executor (versioned in the plugin), so
|
|
96
|
+
// the target is thin config — no baked-in command list.
|
|
97
|
+
expect(target.executor).toBe('@abgov/nx-oc:sandbox');
|
|
98
|
+
expect(target.options).toMatchObject({
|
|
99
|
+
sandboxProject: 'test-sandbox',
|
|
100
|
+
registry: 'ghcr.io/test-org',
|
|
101
|
+
appType: 'node',
|
|
102
|
+
});
|
|
103
|
+
// No database key when the app has no database.
|
|
104
|
+
expect(target.options.database).toBeUndefined();
|
|
130
105
|
|
|
131
106
|
// The in-cluster BuildConfig flow is gone.
|
|
132
|
-
expect(cmds.some((c) => c.includes('oc start-build'))).toBeFalsy();
|
|
133
107
|
expect(host.exists('.openshift/test/sandbox-build.yml')).toBeFalsy();
|
|
108
|
+
});
|
|
134
109
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
).
|
|
110
|
+
it('writes a SANDBOX.md deploy/troubleshooting runbook next to the manifests', async () => {
|
|
111
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
112
|
+
addNodeProject(host);
|
|
113
|
+
|
|
114
|
+
await generator(host, options);
|
|
115
|
+
|
|
116
|
+
expect(host.exists('.openshift/test/SANDBOX.md')).toBeTruthy();
|
|
117
|
+
const doc = host.read('.openshift/test/SANDBOX.md').toString();
|
|
118
|
+
expect(doc).toContain('nx run test:sandbox');
|
|
119
|
+
// resume flags + the copy-paste manual-completion sequence
|
|
120
|
+
expect(doc).toContain('--skipBuild');
|
|
121
|
+
expect(doc).toContain('oc import-image test:sandbox');
|
|
122
|
+
expect(doc).toContain('ghcr.io/test-org/test-sandbox-test:sandbox');
|
|
123
|
+
// no unrendered EJS tags leaked through
|
|
124
|
+
expect(doc).not.toContain('<%');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('SANDBOX.md is app-type aware (frontend redirect URI; node+db migrate logs)', async () => {
|
|
128
|
+
const fe = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
129
|
+
addFrontendProject(fe);
|
|
130
|
+
await generator(fe, options);
|
|
131
|
+
const feDoc = fe.read('.openshift/test/SANDBOX.md').toString();
|
|
132
|
+
expect(feDoc).toContain('Invalid redirect_uri');
|
|
133
|
+
expect(feDoc).not.toContain('-migrate');
|
|
134
|
+
|
|
135
|
+
const node = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
136
|
+
addNodeProject(node);
|
|
137
|
+
await generator(node, { ...options, database: 'postgres' });
|
|
138
|
+
const nodeDoc = node.read('.openshift/test/SANDBOX.md').toString();
|
|
139
|
+
expect(nodeDoc).toContain('test-migrate');
|
|
140
|
+
expect(nodeDoc).not.toContain('Invalid redirect_uri');
|
|
144
141
|
});
|
|
145
142
|
|
|
146
143
|
it('persists the resolved registry to nx.json for reuse', async () => {
|
|
@@ -194,16 +191,10 @@ describe('Sandbox Generator', () => {
|
|
|
194
191
|
expect(appManifest).toContain('sandbox-postgres-creds');
|
|
195
192
|
expect(appManifest).toContain('$(POSTGRES_PASSWORD)');
|
|
196
193
|
|
|
194
|
+
// The database type is passed to the executor, which provisions the shared
|
|
195
|
+
// instance + per-app database at deploy time.
|
|
197
196
|
const config = readProjectConfiguration(host, 'test');
|
|
198
|
-
|
|
199
|
-
expect(cmds.some((c) => c.includes('sandbox-postgres-creds'))).toBeTruthy();
|
|
200
|
-
expect(cmds.some((c) => c.includes('sandbox-postgres.yml'))).toBeTruthy();
|
|
201
|
-
// The per-app database is created idempotently before the app deploys.
|
|
202
|
-
expect(cmds.some((c) => c.includes('createdb -U postgres test_sandbox'))).toBeTruthy();
|
|
203
|
-
const createDbIdx = cmds.findIndex((c) => c.includes('createdb'));
|
|
204
|
-
const rolloutIdx = cmds.findIndex((c) => c.includes('rollout status deployment/test'));
|
|
205
|
-
expect(createDbIdx).toBeGreaterThanOrEqual(0);
|
|
206
|
-
expect(createDbIdx).toBeLessThan(rolloutIdx);
|
|
197
|
+
expect(config.targets['sandbox'].options.database).toBe('postgres');
|
|
207
198
|
});
|
|
208
199
|
|
|
209
200
|
it('generates shared mongodb manifest with secret-backed MONGODB_URI', async () => {
|
|
@@ -222,38 +213,7 @@ describe('Sandbox Generator', () => {
|
|
|
222
213
|
expect(appManifest).toContain('$(MONGO_PASSWORD)');
|
|
223
214
|
|
|
224
215
|
const config = readProjectConfiguration(host, 'test');
|
|
225
|
-
|
|
226
|
-
expect(cmds.some((c) => c.includes('sandbox-mongodb-creds'))).toBeTruthy();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('ensures paired backend Services from proxy-service tags', async () => {
|
|
230
|
-
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
231
|
-
addFrontendProject(host, ['adsp:proxy-service:test-service:3333']);
|
|
232
|
-
|
|
233
|
-
await generator(host, options);
|
|
234
|
-
|
|
235
|
-
const config = readProjectConfiguration(host, 'test');
|
|
236
|
-
const cmds: string[] = config.targets['sandbox'].options.commands;
|
|
237
|
-
const guard = cmds.find((c) => c.includes('oc get service test-service'));
|
|
238
|
-
expect(guard).toBeTruthy();
|
|
239
|
-
// idempotent: only creates the Service (for DNS) when it's missing — no
|
|
240
|
-
// backend deployment, which would have no image until its own sandbox runs.
|
|
241
|
-
expect(guard).toContain('||');
|
|
242
|
-
expect(guard).toContain(
|
|
243
|
-
'oc create service clusterip test-service --tcp=3333:3333'
|
|
244
|
-
);
|
|
245
|
-
expect(guard).not.toContain('test-service.yml');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('adds no paired-service guard when there are no proxy-service tags', async () => {
|
|
249
|
-
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
250
|
-
addNodeProject(host);
|
|
251
|
-
|
|
252
|
-
await generator(host, options);
|
|
253
|
-
|
|
254
|
-
const config = readProjectConfiguration(host, 'test');
|
|
255
|
-
const cmds: string[] = config.targets['sandbox'].options.commands;
|
|
256
|
-
expect(cmds.some((c) => c.includes('oc get service'))).toBeFalsy();
|
|
216
|
+
expect(config.targets['sandbox'].options.database).toBe('mongo');
|
|
257
217
|
});
|
|
258
218
|
|
|
259
219
|
it('registers the deployment Route redirect URI for a frontend client', async () => {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = convertSandboxTarget;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
// Converts sandbox targets generated as raw `nx:run-commands` command lists into
|
|
7
|
+
// the `@abgov/nx-oc:sandbox` executor, so the versioned deploy logic (import
|
|
8
|
+
// retry, preflight, resume flags) applies without re-running the generator.
|
|
9
|
+
//
|
|
10
|
+
// sandboxProject and registry are recovered from the old command strings; the
|
|
11
|
+
// app type is left to the executor to detect at run time.
|
|
12
|
+
function convertSandboxTarget(tree) {
|
|
13
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
var _a, _b, _c, _d;
|
|
15
|
+
let converted = 0;
|
|
16
|
+
for (const [name, project] of (0, devkit_1.getProjects)(tree)) {
|
|
17
|
+
const target = (_a = project.targets) === null || _a === void 0 ? void 0 : _a['sandbox'];
|
|
18
|
+
if (!target || target.executor !== 'nx:run-commands')
|
|
19
|
+
continue;
|
|
20
|
+
const commands = Array.isArray((_b = target.options) === null || _b === void 0 ? void 0 : _b.commands)
|
|
21
|
+
? target.options.commands
|
|
22
|
+
: [];
|
|
23
|
+
const joined = commands.join('\n');
|
|
24
|
+
// Only touch the sandbox target this plugin generated (podman build + import).
|
|
25
|
+
if (!/podman build/.test(joined) || !/oc import-image/.test(joined))
|
|
26
|
+
continue;
|
|
27
|
+
const namespace = (_c = joined.match(/-n\s+(\S+)/)) === null || _c === void 0 ? void 0 : _c[1];
|
|
28
|
+
const imageRef = (_d = joined.match(/podman build[^\n]*?-t\s+(\S+)/)) === null || _d === void 0 ? void 0 : _d[1];
|
|
29
|
+
if (!namespace || !imageRef) {
|
|
30
|
+
devkit_1.logger.warn(`[nx-oc] Could not parse the sandbox target for "${name}"; leaving it as run-commands. Re-run the sandbox generator to migrate it.`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// imageRef is <registry>/<namespace>-<project>:<tag>; the registry is
|
|
34
|
+
// everything before the final path segment (it contains its own slash,
|
|
35
|
+
// e.g. ghcr.io/org).
|
|
36
|
+
const registry = imageRef.slice(0, imageRef.lastIndexOf('/'));
|
|
37
|
+
const database = /sandbox-postgres/.test(joined)
|
|
38
|
+
? 'postgres'
|
|
39
|
+
: /sandbox-mongodb/.test(joined)
|
|
40
|
+
? 'mongo'
|
|
41
|
+
: undefined;
|
|
42
|
+
project.targets['sandbox'] = {
|
|
43
|
+
executor: '@abgov/nx-oc:sandbox',
|
|
44
|
+
options: Object.assign({ sandboxProject: namespace, registry }, (database ? { database } : {})),
|
|
45
|
+
};
|
|
46
|
+
(0, devkit_1.updateProjectConfiguration)(tree, name, project);
|
|
47
|
+
converted++;
|
|
48
|
+
}
|
|
49
|
+
if (converted > 0) {
|
|
50
|
+
devkit_1.logger.info(`[nx-oc] Converted ${converted} sandbox target(s) to the @abgov/nx-oc:sandbox executor.`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=convert-sandbox-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert-sandbox-target.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/migrations/convert-sandbox-target/convert-sandbox-target.ts"],"names":[],"mappings":";;AAaA,uCAiDC;;AA9DD,uCAKoB;AAEpB,iFAAiF;AACjF,6EAA6E;AAC7E,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,0DAA0D;AAC1D,SAA8B,oBAAoB,CAAC,IAAU;;;QAC3D,IAAI,SAAS,GAAG,CAAC,CAAC;QAElB,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAA,oBAAW,EAAC,IAAI,CAAC,EAAE,CAAC;YAChD,MAAM,MAAM,GAAG,MAAA,OAAO,CAAC,OAAO,0CAAG,SAAS,CAAC,CAAC;YAC5C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,iBAAiB;gBAAE,SAAS;YAE/D,MAAM,QAAQ,GAAa,KAAK,CAAC,OAAO,CAAC,MAAA,MAAM,CAAC,OAAO,0CAAE,QAAQ,CAAC;gBAChE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ;gBACzB,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnC,+EAA+E;YAC/E,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;gBAAE,SAAS;YAE9E,MAAM,SAAS,GAAG,MAAA,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,0CAAG,CAAC,CAAC,CAAC;YAClD,MAAM,QAAQ,GAAG,MAAA,MAAM,CAAC,KAAK,CAAC,+BAA+B,CAAC,0CAAG,CAAC,CAAC,CAAC;YACpE,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC5B,eAAM,CAAC,IAAI,CACT,mDAAmD,IAAI,4EAA4E,CACpI,CAAC;gBACF,SAAS;YACX,CAAC;YACD,sEAAsE;YACtE,uEAAuE;YACvE,qBAAqB;YACrB,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC9C,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;oBAChC,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,SAAS,CAAC;YAEd,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG;gBAC3B,QAAQ,EAAE,sBAAsB;gBAChC,OAAO,kBACL,cAAc,EAAE,SAAS,EACzB,QAAQ,IACL,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAClC;aACF,CAAC;YACF,IAAA,mCAA0B,EAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YAChD,SAAS,EAAE,CAAC;QACd,CAAC;QAED,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,eAAM,CAAC,IAAI,CACT,qBAAqB,SAAS,0DAA0D,CACzF,CAAC;QACJ,CAAC;IACH,CAAC;CAAA"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addProjectConfiguration,
|
|
3
|
+
readProjectConfiguration,
|
|
4
|
+
Tree,
|
|
5
|
+
} from '@nx/devkit';
|
|
6
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
7
|
+
import migration from './convert-sandbox-target';
|
|
8
|
+
|
|
9
|
+
// A representative slice of the old generated run-commands sandbox target.
|
|
10
|
+
function oldSandboxCommands(ns: string, database?: 'postgres' | 'mongo') {
|
|
11
|
+
const ref = `ghcr.io/test-org/${ns}-app:sandbox`;
|
|
12
|
+
return [
|
|
13
|
+
...(database === 'postgres'
|
|
14
|
+
? [`oc apply -f .openshift/sandbox/sandbox-postgres.yml -n ${ns}`]
|
|
15
|
+
: []),
|
|
16
|
+
...(database === 'mongo'
|
|
17
|
+
? [`oc apply -f .openshift/sandbox/sandbox-mongodb.yml -n ${ns}`]
|
|
18
|
+
: []),
|
|
19
|
+
`npx nx build app --configuration production`,
|
|
20
|
+
`podman build --platform=linux/amd64 -f .openshift/app/Dockerfile -t ${ref} .`,
|
|
21
|
+
`podman push ${ref}`,
|
|
22
|
+
`oc tag ${ref} app:sandbox --reference-policy=local -n ${ns}`,
|
|
23
|
+
`n=0; until oc import-image app:sandbox --confirm -n ${ns}; do n=$((n+1)); [ $n -ge 5 ] && exit 1; sleep 3; done`,
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('convert-sandbox-target migration', () => {
|
|
28
|
+
let tree: Tree;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('converts a run-commands sandbox target to the executor', async () => {
|
|
34
|
+
addProjectConfiguration(tree, 'app', {
|
|
35
|
+
root: 'apps/app',
|
|
36
|
+
projectType: 'application',
|
|
37
|
+
targets: {
|
|
38
|
+
sandbox: {
|
|
39
|
+
executor: 'nx:run-commands',
|
|
40
|
+
options: { commands: oldSandboxCommands('my-ns'), parallel: false },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await migration(tree);
|
|
46
|
+
|
|
47
|
+
const target = readProjectConfiguration(tree, 'app').targets.sandbox;
|
|
48
|
+
expect(target.executor).toBe('@abgov/nx-oc:sandbox');
|
|
49
|
+
expect(target.options).toEqual({
|
|
50
|
+
sandboxProject: 'my-ns',
|
|
51
|
+
registry: 'ghcr.io/test-org',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('recovers the database type from the old commands', async () => {
|
|
56
|
+
addProjectConfiguration(tree, 'app', {
|
|
57
|
+
root: 'apps/app',
|
|
58
|
+
projectType: 'application',
|
|
59
|
+
targets: {
|
|
60
|
+
sandbox: {
|
|
61
|
+
executor: 'nx:run-commands',
|
|
62
|
+
options: { commands: oldSandboxCommands('my-ns', 'postgres') },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await migration(tree);
|
|
68
|
+
|
|
69
|
+
expect(readProjectConfiguration(tree, 'app').targets.sandbox.options).toEqual({
|
|
70
|
+
sandboxProject: 'my-ns',
|
|
71
|
+
registry: 'ghcr.io/test-org',
|
|
72
|
+
database: 'postgres',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('leaves an already-migrated executor target untouched', async () => {
|
|
77
|
+
const executorTarget = {
|
|
78
|
+
executor: '@abgov/nx-oc:sandbox',
|
|
79
|
+
options: { sandboxProject: 'my-ns', registry: 'ghcr.io/test-org' },
|
|
80
|
+
};
|
|
81
|
+
addProjectConfiguration(tree, 'app', {
|
|
82
|
+
root: 'apps/app',
|
|
83
|
+
projectType: 'application',
|
|
84
|
+
targets: { sandbox: executorTarget },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await migration(tree);
|
|
88
|
+
|
|
89
|
+
expect(readProjectConfiguration(tree, 'app').targets.sandbox).toEqual(
|
|
90
|
+
executorTarget
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('ignores unrelated run-commands targets', async () => {
|
|
95
|
+
const unrelated = {
|
|
96
|
+
executor: 'nx:run-commands',
|
|
97
|
+
options: { commands: ['echo hi'] },
|
|
98
|
+
};
|
|
99
|
+
addProjectConfiguration(tree, 'app', {
|
|
100
|
+
root: 'apps/app',
|
|
101
|
+
projectType: 'application',
|
|
102
|
+
targets: { sandbox: unrelated },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await migration(tree);
|
|
106
|
+
|
|
107
|
+
expect(readProjectConfiguration(tree, 'app').targets.sandbox).toEqual(
|
|
108
|
+
unrelated
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|