@abgov/nx-oc 13.0.0-beta.2 → 13.0.0-beta.3

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.3",
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,183 @@
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
+ // oc tag triggers an async imagestream reconcile, so a back-to-back
51
+ // import-image can 409 ("object has been modified"). Retry until it settles.
52
+ function importWithRetry(imageStreamTag, namespace, cwd, retries) {
53
+ for (let attempt = 1;; attempt++) {
54
+ try {
55
+ run(`Import image (attempt ${attempt}/${retries})`, `oc import-image ${imageStreamTag} --confirm -n ${namespace}`, cwd);
56
+ return;
57
+ }
58
+ catch (err) {
59
+ if (attempt >= retries) {
60
+ throw new Error(`oc import-image failed after ${retries} attempts: ${err.message}`);
61
+ }
62
+ devkit_1.logger.warn(` import-image failed (likely the oc tag reconcile race); retrying in 3s`);
63
+ (0, child_process_1.execSync)('sleep 3', { stdio: 'ignore' });
64
+ }
65
+ }
66
+ }
67
+ function runExecutor(options, context) {
68
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
69
+ var _a, _b, _c, _d;
70
+ const projectName = context.projectName;
71
+ const project = projectName && ((_a = context.projectsConfigurations) === null || _a === void 0 ? void 0 : _a.projects[projectName]);
72
+ if (!projectName || !project) {
73
+ devkit_1.logger.error('[nx-oc:sandbox] Could not resolve the target project.');
74
+ return { success: false };
75
+ }
76
+ const { sandboxProject, registry, imageTag = 'sandbox', skipBuild = false, skipPush = false, importRetries = 5, } = options;
77
+ if (!sandboxProject) {
78
+ devkit_1.logger.error('[nx-oc:sandbox] "sandboxProject" is required.');
79
+ return { success: false };
80
+ }
81
+ if (!registry) {
82
+ devkit_1.logger.error('[nx-oc:sandbox] "registry" is required.');
83
+ return { success: false };
84
+ }
85
+ const appType = (_b = options.appType) !== null && _b !== void 0 ? _b : (0, app_type_1.detectApplicationType)(project);
86
+ if (!appType) {
87
+ devkit_1.logger.error(`[nx-oc:sandbox] Could not determine the application type for ${projectName}.`);
88
+ return { success: false };
89
+ }
90
+ const database = (_c = options.database) !== null && _c !== void 0 ? _c : 'none';
91
+ // Container registries (GHCR) require lowercase paths. Prefix the image with
92
+ // the (per-user) namespace so images from different experimenters never
93
+ // collide on GHCR's org-global package names.
94
+ const reg = registry.toLowerCase();
95
+ const registryHost = reg.split('/')[0];
96
+ const imageName = `${sandboxProject}-${projectName}`.toLowerCase();
97
+ const imageRef = `${reg}/${imageName}:${imageTag}`;
98
+ const projectRoot = project.root;
99
+ const cwd = context.root;
100
+ try {
101
+ // ---- preflight: check prerequisites before the expensive build ----
102
+ (0, oc_utils_1.ensureOcLogin)();
103
+ // The pull secret + import always need gh (session token to reach GHCR).
104
+ requireGhAuth();
105
+ if (!skipBuild || !skipPush) {
106
+ requirePodman();
107
+ }
108
+ // ---- service client secret (node services authenticate to ADSP) ----
109
+ // Upserted from the current .env so re-runs pick up a rotated secret.
110
+ if (appType === 'node') {
111
+ run('Upsert CLIENT_SECRET', `oc create secret generic ${projectName}-secrets ` +
112
+ `--from-literal=CLIENT_SECRET="$(grep -E '^CLIENT_SECRET=' ${projectRoot}/.env 2>/dev/null | cut -d= -f2-)" ` +
113
+ `-n ${sandboxProject} --dry-run=client -o yaml | oc apply -f -`, cwd);
114
+ }
115
+ // ---- shared database provisioning ----
116
+ if (database === 'postgres') {
117
+ run('Ensure Postgres credentials', `oc get secret sandbox-postgres-creds -n ${sandboxProject} 2>/dev/null || ` +
118
+ `oc create secret generic sandbox-postgres-creds ` +
119
+ `--from-literal=POSTGRESQL_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
120
+ `-n ${sandboxProject}`, cwd);
121
+ run('Deploy shared Postgres', `oc apply -f .openshift/sandbox/sandbox-postgres.yml -n ${sandboxProject}`, cwd);
122
+ // The shared Postgres only creates the admin database; each app needs its
123
+ // own <app>_sandbox database created before its migrate init container
124
+ // runs. Wait for Postgres, then create the database idempotently.
125
+ const dbName = `${projectName}_sandbox`;
126
+ run('Create app database', `oc rollout status deployment/sandbox-postgres -n ${sandboxProject} --timeout=180s && ` +
127
+ `oc exec -n ${sandboxProject} deployment/sandbox-postgres -- ` +
128
+ `bash -lc "psql -U postgres -tc \\"SELECT 1 FROM pg_database WHERE datname='${dbName}'\\" ` +
129
+ `| grep -q 1 || createdb -U postgres ${dbName}"`, cwd);
130
+ }
131
+ else if (database === 'mongo') {
132
+ run('Ensure MongoDB credentials', `oc get secret sandbox-mongodb-creds -n ${sandboxProject} 2>/dev/null || ` +
133
+ `oc create secret generic sandbox-mongodb-creds ` +
134
+ `--from-literal=MONGODB_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
135
+ `-n ${sandboxProject}`, cwd);
136
+ run('Deploy shared MongoDB', `oc apply -f .openshift/sandbox/sandbox-mongodb.yml -n ${sandboxProject}`, cwd);
137
+ }
138
+ // ---- paired backend Services (so a frontend's nginx resolves proxy_pass
139
+ // upstreams at startup). Only the Service is needed for DNS. Idempotent. ----
140
+ const proxyServices = ((_d = project.tags) !== null && _d !== void 0 ? _d : [])
141
+ .filter((tag) => tag.startsWith(PROXY_TAG_PREFIX))
142
+ .map((tag) => {
143
+ const value = tag.slice(PROXY_TAG_PREFIX.length);
144
+ const lastColon = value.lastIndexOf(':');
145
+ return { name: value.slice(0, lastColon), port: value.slice(lastColon + 1) };
146
+ });
147
+ for (const { name, port } of proxyServices) {
148
+ run(`Ensure paired Service ${name}`, `oc get service ${name} -n ${sandboxProject} >/dev/null 2>&1 || ` +
149
+ `oc create service clusterip ${name} --tcp=${port}:${port} -n ${sandboxProject}`, cwd);
150
+ }
151
+ // ---- build locally + push to the registry ----
152
+ if (!skipBuild) {
153
+ run('Build', `npx nx build ${projectName} --configuration production`, cwd);
154
+ run('Podman build', `podman build --platform=linux/amd64 -f .openshift/${projectName}/Dockerfile -t ${imageRef} .`, cwd);
155
+ }
156
+ if (!skipPush) {
157
+ // gh supplies the token so no PAT is stored; the same session token backs
158
+ // the pull secret below.
159
+ run('Registry login', `gh auth token | podman login ${registryHost} -u "$(gh api user -q .login)" --password-stdin`, cwd);
160
+ run('Push image', `podman push ${imageRef}`, cwd);
161
+ }
162
+ // ---- import into the namespace imagestream + roll out ----
163
+ // Per-deploy pull secret from the gh session (no long-lived PAT needed).
164
+ run('Upsert pull secret', `oc create secret docker-registry ghcr-pull ` +
165
+ `--docker-server=${registryHost} --docker-username="$(gh api user -q .login)" --docker-password="$(gh auth token)" ` +
166
+ `-n ${sandboxProject} --dry-run=client -o yaml | oc apply -f -`, cwd);
167
+ // oc tag sets/repoints the imagestream tag with reference-policy=local so
168
+ // pods pull in-cluster; import --confirm then pulls the current manifest.
169
+ run('Tag imagestream', `oc tag ${imageRef} ${projectName}:${imageTag} --reference-policy=local -n ${sandboxProject}`, cwd);
170
+ importWithRetry(`${projectName}:${imageTag}`, sandboxProject, cwd, importRetries);
171
+ run('Apply manifest', `oc process -f .openshift/${projectName}/${projectName}.yml -p PROJECT=${sandboxProject} | oc apply -f -`, cwd);
172
+ run('Restart rollout', `oc rollout restart deployment/${projectName} -n ${sandboxProject}`, cwd);
173
+ run('Wait for rollout', `oc rollout status deployment/${projectName} -n ${sandboxProject} --timeout=180s`, cwd);
174
+ devkit_1.logger.info(`\n✓ Sandbox deploy complete for ${projectName}.`);
175
+ return { success: true };
176
+ }
177
+ catch (err) {
178
+ devkit_1.logger.error(`\n[nx-oc:sandbox] ${err.message}`);
179
+ return { success: false };
180
+ }
181
+ });
182
+ }
183
+ //# 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,186 @@
1
+ import { ExecutorContext } 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
+ });
@@ -0,0 +1,12 @@
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
+ importRetries?: number;
12
+ }
@@ -0,0 +1,50 @@
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
+ "importRetries": {
44
+ "type": "number",
45
+ "default": 5,
46
+ "description": "How many times to retry oc import-image (absorbs the 409 from oc tag's async reconcile)."
47
+ }
48
+ },
49
+ "required": ["sandboxProject", "registry"]
50
+ }
@@ -0,0 +1,91 @@
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
+ ```
29
+
30
+ ## Preflight failures — cause → fix
31
+
32
+ - **`'podman' is required …`** — podman not installed. `brew install podman` (macOS).
33
+ - **`podman is installed but not responding`** — machine stopped. `podman machine start`.
34
+ - **`'gh' is required …`** — GitHub CLI not installed. See https://cli.github.com.
35
+ - **`gh is installed but not authenticated`** — run `gh auth login`.
36
+ - **oc login prompts / fails** — `oc login` to your cluster, targeting the
37
+ `<%= sandboxProject %>` namespace (`oc project <%= sandboxProject %>`).
38
+
39
+ ## If the deploy fails mid-way (work around by hand)
40
+
41
+ The executor stops at the first failing step. After fixing the cause, the
42
+ fastest path is usually `--skipBuild` (or `--skipBuild --skipPush`) to resume.
43
+ If you need to drive it manually, these are the remaining steps — the image ref
44
+ is `<%= registry %>/<%= imageName %>:sandbox`:
45
+
46
+ ```bash
47
+ NS=<%= sandboxProject %>
48
+ REF=<%= registry %>/<%= imageName %>:sandbox
49
+ # (1) push, if not already pushed
50
+ gh auth token | podman login <%= registryHost %> -u "$(gh api user -q .login)" --password-stdin
51
+ podman push "$REF"
52
+ # (2) point the imagestream at it and import (retry absorbs the tag race)
53
+ oc tag "$REF" <%= projectName %>:sandbox --reference-policy=local -n "$NS"
54
+ until oc import-image <%= projectName %>:sandbox --confirm -n "$NS"; do sleep 3; done
55
+ # (3) apply the manifest and roll out
56
+ oc process -f .openshift/<%= projectName %>/<%= projectName %>.yml -p PROJECT="$NS" | oc apply -f -
57
+ oc rollout restart deployment/<%= projectName %> -n "$NS"
58
+ oc rollout status deployment/<%= projectName %> -n "$NS" --timeout=180s
59
+ ```
60
+
61
+ ## Verify
62
+
63
+ ```bash
64
+ oc get pods -n <%= sandboxProject %> -l name=<%= projectName %>
65
+ <% 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}')/"<% } %>
66
+ ```
67
+
68
+ ## Known issues & workarounds
69
+
70
+ - **`podman push` / import fails with an auth error** — the *active* `gh`
71
+ account lacks `write:packages` (or access to the org). Preflight only confirms
72
+ *an* account is logged in, not its scope. Check `gh auth status`; switch with
73
+ `gh auth switch -u <account>`, then re-run with `--skipBuild`.
74
+ - **Pod `FailedCreate` "exceeded quota"** — the namespace CPU quota is full.
75
+ Free room: `oc get deploy -n <%= sandboxProject %>` then delete stale apps
76
+ (`oc delete all,configmap,is -l app=<old-app> -n <%= sandboxProject %>`), and re-run.
77
+ - **Pod `CrashLoopBackOff`** — `oc logs -n <%= sandboxProject %> <pod> -c <%= projectName %>`.<% if (appType === 'node' && database && database !== 'none') { %>
78
+ For the DB migration, also check the init container:
79
+ `oc logs -n <%= sandboxProject %> <pod> -c <%= projectName %>-migrate`.<% } %>
80
+ <% if (appType === 'frontend') { %>- **Sign-in returns `Invalid redirect_uri`** — the sandbox Route wasn't
81
+ registered on the Keycloak public client (the generator does this when it can
82
+ resolve the cluster ingress domain). Add `https://<%= projectName %>-<%= sandboxProject %>.<ingress-domain>/*`
83
+ to the client's valid redirect URIs.
84
+ <% } %>- **Import 409 "object has been modified"** — the `oc tag` reconcile race; the
85
+ executor already retries. If you hit it manually, just re-run the import.
86
+
87
+ ## Teardown
88
+
89
+ ```bash
90
+ nx run <%= projectName %>:sandbox-teardown # remove sandbox resources + delete the GHCR image
91
+ ```
@@ -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":";;AAsUA,4BAYC;;AAlVD,uCASoB;AACpB,6BAA6B;AAC7B,qCAAyE;AACzE,qDAI+B;AAC/B,mDAA+D;AAC/D,mDAAiF;AAGjF,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAEjD,gFAAgF;AAChF,2EAA2E;AAC3E,kFAAkF;AAClF,SAAe,eAAe,CAC5B,IAAU,EACV,QAA4B,EAC5B,SAA6B;;;QAE7B,IAAI,QAAQ;YAAE,OAAO,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAErD,MAAM,MAAM,GAAG,MAAA,MACb,MAAA,IAAA,mBAAU,EAAC,IAAI,CAAC,0CAAE,UAGnB,0CAAG,iBAAiB,CAAC,0CAAE,QAAQ,CAAC;QACjC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,OAAO,GAAG,IAAA,oCAAwB,EAAC,SAAS,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CAAC,yBAAyB,OAAO,CAAC,WAAW,EAAE,8BAA8B,CAAC,CAAC;YAC1F,OAAO,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,CAAC;QAED,MAAM,EAAE,MAAM,EAAE,GAAG,2CAAa,UAAU,EAAC,CAAC;QAC5C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAuB;YAChE,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,UAAU;YAChB,OAAO,EACL,uFAAuF;SAC1F,CAAC,CAAC;QACH,OAAO,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;CAAA;AAED,8EAA8E;AAC9E,SAAS,eAAe,CAAC,IAAU,EAAE,QAAgB;;IACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,MAAA,IAAA,mBAAU,EAAC,IAAI,CAAC,mCAAI,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,CAAC,MAAA,MAAM,CAAC,UAAU,mCAAI,EAAE,CAG1C,CAAC;IACF,UAAU,CAAC,iBAAiB,CAAC,mCACxB,CAAC,MAAA,UAAU,CAAC,iBAAiB,CAAC,mCAAI,EAAE,CAAC,KACxC,QAAQ,EAAE,KAAK,GAChB,CAAC;IACF,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,IAAA,qBAAY,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAe,gBAAgB,CAC7B,IAAU,EACV,OAAe;;;QAEf,MAAM,WAAW,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;QAEpD,MAAM,MAAM,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAC3D,MAAM,OAAO,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,IAAA,gCAAqB,EAAC,MAAM,CAAC,CAAC;QAEjE,MAAM,IAAI,GAAG,MAAM,IAAA,2BAAoB,EAAC,IAAI,kCACvC,OAAO,KACV,GAAG,EAAE,MAAC,OAAO,CAAC,GAA+B,mCAAI,KAAK,IACtD,CAAC;QAEH,MAAM,SAAS,GAAG,IAAA,2BAAe,GAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,CAAC,MAAM,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1F,wEAAwE;QACxE,4EAA4E;QAC5E,MAAM,SAAS,GAAG,GAAG,OAAO,CAAC,cAAc,IAAI,WAAW,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3E,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,SAAS,CAAC,CAAC;QAE1C,uCACK,OAAO,KACV,OAAO;YACP,IAAI;YACJ,WAAW,EACX,eAAe,EAAE,IAAA,6BAAkB,EAAC,MAAM,CAAC,EAC3C,QAAQ,EACR,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACpC,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EACnD,SAAS,EACT,QAAQ,EAAE,GAAG,QAAQ,IAAI,SAAS,UAAU,EAC5C,mBAAmB,EAAE,QAAQ,CAAC,CAAC,CAAC,sBAAsB,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS,IAC5E;IACJ,CAAC;CAAA;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAyB;;IAC7D,MAAM,eAAe,iDAChB,OAAO,GACP,OAAO,CAAC,IAAI;QACf,0EAA0E;QAC1E,mBAAmB,EAAE,MAAA,OAAO,CAAC,mBAAmB,mCAAI,EAAE,EACtD,QAAQ,EAAE,MAAA,OAAO,CAAC,QAAQ,mCAAI,MAAM,EACpC,OAAO,EAAE,IAAI,EACb,cAAc,EAAE,OAAO,CAAC,cAAc,EACtC,IAAI,EAAE,EAAE,GACT,CAAC;IACF,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,OAAO,CAAC,OAAO,QAAQ,CAAC,EAC9D,gBAAgB,OAAO,CAAC,WAAW,EAAE,EACrC,eAAe,CAChB,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAyB;IAC7D,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,KAAK,MAAM;QAAE,OAAO;IAC7D,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,EACtC,sBAAsB,EACtB,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CACzC,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAyB;;IAC7D,MAAM,MAAM,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,EACJ,WAAW,EACX,cAAc,EACd,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,SAAS,GACV,GAAG,OAAO,CAAC;IAEZ,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,2EAA2E;IAC3E,8EAA8E;IAC9E,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CACX,4BAA4B,WAAW,WAAW;YAChD,6DAA6D,MAAM,CAAC,IAAI,qCAAqC;YAC7G,MAAM,cAAc,2CAA2C,CAClE,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CACX,2CAA2C,cAAc,kBAAkB;YACzE,kDAAkD;YAClD,oFAAoF;YACpF,MAAM,cAAc,EAAE,CACzB,CAAC;QACF,QAAQ,CAAC,IAAI,CACX,0DAA0D,cAAc,EAAE,CAC3E,CAAC;QACF,yEAAyE;QACzE,uEAAuE;QACvE,sEAAsE;QACtE,+DAA+D;QAC/D,MAAM,MAAM,GAAG,GAAG,WAAW,UAAU,CAAC;QACxC,QAAQ,CAAC,IAAI,CACX,oDAAoD,cAAc,qBAAqB;YACrF,cAAc,cAAc,kCAAkC;YAC9D,8EAA8E,MAAM,OAAO;YAC3F,uCAAuC,MAAM,GAAG,CACnD,CAAC;IACJ,CAAC;SAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CACX,0CAA0C,cAAc,kBAAkB;YACxE,iDAAiD;YACjD,iFAAiF;YACjF,MAAM,cAAc,EAAE,CACzB,CAAC;QACF,QAAQ,CAAC,IAAI,CACX,yDAAyD,cAAc,EAAE,CAC1E,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,4EAA4E;IAC5E,2EAA2E;IAC3E,wEAAwE;IACxE,2EAA2E;IAC3E,8EAA8E;IAC9E,oEAAoE;IACpE,MAAM,MAAM,GAAG,qBAAqB,CAAC;IACrC,MAAM,aAAa,GAAG,CAAC,MAAA,MAAM,CAAC,IAAI,mCAAI,EAAE,CAAC;SACtC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;SACvC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC;YAC/B,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;SACjC,CAAC;IACJ,CAAC,CAAC,CAAC;IACL,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,aAAa,EAAE,CAAC;QAC3C,QAAQ,CAAC,IAAI,CACX,kBAAkB,IAAI,OAAO,cAAc,sBAAsB;YAC/D,+BAA+B,IAAI,UAAU,IAAI,IAAI,IAAI,OAAO,cAAc,EAAE,CACnF,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,+EAA+E;IAC/E,6EAA6E;IAC7E,wEAAwE;IACxE,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,gBAAgB,WAAW,6BAA6B,CAAC,CAAC;IACxE,QAAQ,CAAC,IAAI,CACX,qDAAqD,WAAW,kBAAkB,QAAQ,IAAI,CAC/F,CAAC;IACF,+EAA+E;IAC/E,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,CACX,gCAAgC,YAAY,iDAAiD,CAC9F,CAAC;IACF,QAAQ,CAAC,IAAI,CAAC,eAAe,QAAQ,EAAE,CAAC,CAAC;IACzC,6EAA6E;IAC7E,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CACX,6CAA6C;QAC3C,mBAAmB,YAAY,qFAAqF;QACpH,MAAM,cAAc,2CAA2C,CAClE,CAAC;IACF,8EAA8E;IAC9E,oEAAoE;IACpE,QAAQ,CAAC,IAAI,CACX,UAAU,QAAQ,IAAI,WAAW,wCAAwC,cAAc,EAAE,CAC1F,CAAC;IACF,oEAAoE;IACpE,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CACX,8BAA8B,WAAW,yBAAyB,cAAc,OAAO;QACrF,mDAAmD,CACtD,CAAC;IAEF,QAAQ,CAAC,IAAI,CACX,4BAA4B,WAAW,IAAI,WAAW,mBAAmB,cAAc,kBAAkB,CAC1G,CAAC;IAEF,QAAQ,CAAC,IAAI,CACX,iCAAiC,WAAW,OAAO,cAAc,EAAE,CACpE,CAAC;IACF,QAAQ,CAAC,IAAI,CACX,gCAAgC,WAAW,OAAO,cAAc,iBAAiB,CAClF,CAAC;IAEF,MAAM,CAAC,OAAO,mCACT,MAAM,CAAC,OAAO,KACjB,OAAO,EAAE;YACP,QAAQ,EAAE,iBAAiB;YAC3B,OAAO,EAAE;gBACP,QAAQ;gBACR,QAAQ,EAAE,KAAK;aAChB;SACF,EACD,kBAAkB,EAAE;YAClB,QAAQ,EAAE,iBAAiB;YAC3B,OAAO,EAAE;gBACP,QAAQ,EAAE;oBACR,qCAAqC,WAAW,OAAO,cAAc,qBAAqB;oBAC1F,yBAAyB,WAAW,OAAO,cAAc,qBAAqB;oBAC9E,mEAAmE;oBACnE,yEAAyE;oBACzE,qCAAqC;oBACrC,gCAAgC,WAAW,uBAAuB,SAAS,sBAAsB;iBAClG;aACF;SACF,GACF,CAAC;IAEF,IAAA,mCAA0B,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AAC5D,CAAC;AAED,+EAA+E;AAC/E,2EAA2E;AAC3E,iFAAiF;AACjF,6EAA6E;AAC7E,qCAAqC;AACrC,SAAe,0BAA0B,CAAC,OAAyB;;QACjE,IAAI,OAAO,CAAC,OAAO,KAAK,UAAU;YAAE,OAAO;QAC3C,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;QACtD,IAAI,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,WAAW,CAAA;YAAE,OAAO;QAE/B,MAAM,aAAa,GAAG,IAAA,kCAAuB,GAAE,CAAC;QAChD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CACT,8FAA8F;gBAC5F,2EAA2E,CAC9E,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,WAAW,WAAW,IAAI,cAAc,IAAI,aAAa,EAAE,CAAC;QAC7E,MAAM,QAAQ,GAAG,WAAW,IAAI,CAAC,MAAM,IAAI,WAAW,EAAE,CAAC;QACzD,MAAM,IAAA,4BAAqB,EACzB,IAAI,CAAC,gBAAgB,EACrB,IAAI,CAAC,WAAW,EAChB,QAAQ,EACR,CAAC,GAAG,QAAQ,IAAI,CAAC,EACjB,IAAI,CAAC,WAAW,CACjB,CAAC;IACJ,CAAC;CAAA;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,8DAA8D,CAAC,CAAC;YAC5E,OAAO;QACT,CAAC;QAED,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAC1C,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAC1C,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAC1C,MAAM,0BAA0B,CAAC,iBAAiB,CAAC,CAAC;QACpD,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}
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
+ });