@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 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
  }
@@ -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.2",
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,5 @@
1
+ import { ExecutorContext } from '@nx/devkit';
2
+ import { SandboxExecutorSchema } from './schema';
3
+ export default function runExecutor(options: SandboxExecutorSchema, context: ExecutorContext): Promise<{
4
+ success: boolean;
5
+ }>;
@@ -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, imageRef, registryHost, registryOrg, imageName, } = options;
82
- const commands = [];
83
- // ADSP services authenticate with the access service using a confidential
84
- // Keycloak client secret. The express-service generator writes it to the
85
- // service's .env (CLIENT_SECRET=...) at generate time; mirror it into an
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:run-commands',
166
- options: {
167
- commands,
168
- parallel: false,
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":"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"}
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 target', async () => {
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
- expect(config.targets['sandbox']).toBeTruthy();
94
- expect(config.targets['sandbox'].executor).toBe('nx:run-commands');
95
- // Commands run in order (nx:run-commands has no `sequential` option).
96
- expect(config.targets['sandbox'].options.parallel).toBe(false);
97
- const cmds: string[] = config.targets['sandbox'].options.commands;
98
- expect(cmds.some((c) => c.includes('oc rollout restart'))).toBeTruthy();
99
- expect(cmds.some((c) => c.includes('oc rollout status'))).toBeTruthy();
100
- expect(cmds.some((c) => c.includes('nx build test'))).toBeTruthy();
101
-
102
- // Local podman build + push to the prescribed registry, with the image name
103
- // prefixed by the (per-user) sandbox namespace to avoid GHCR's org-global
104
- // name collisions.
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
- // A node service's ADSP client secret is mirrored from .env into an
136
- // OpenShift Secret the deployment reads.
137
- expect(
138
- cmds.some(
139
- (c) =>
140
- c.includes('oc create secret generic test-secrets') &&
141
- c.includes('CLIENT_SECRET')
142
- )
143
- ).toBeTruthy();
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
- const cmds: string[] = config.targets['sandbox'].options.commands;
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
- const cmds: string[] = config.targets['sandbox'].options.commands;
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,2 @@
1
+ import { Tree } from '@nx/devkit';
2
+ export default function convertSandboxTarget(tree: Tree): Promise<void>;
@@ -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
+ });