@abgov/nx-oc 13.0.0-beta.1 → 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 +5 -0
- package/migrations.json +10 -0
- package/package.json +4 -1
- package/src/adsp/adsp-utils.d.ts +10 -0
- package/src/adsp/adsp-utils.js +48 -0
- package/src/adsp/adsp-utils.js.map +1 -1
- package/src/build-assets.spec.ts +62 -0
- package/src/executors/sandbox/sandbox.d.ts +5 -0
- package/src/executors/sandbox/sandbox.js +183 -0
- package/src/executors/sandbox/sandbox.js.map +1 -0
- package/src/executors/sandbox/sandbox.spec.ts +186 -0
- package/src/executors/sandbox/schema.d.ts +12 -0
- package/src/executors/sandbox/schema.json +50 -0
- package/src/generators/deployment/deployment.js +11 -25
- package/src/generators/deployment/deployment.js.map +1 -1
- package/src/generators/deployment/deployment.spec.ts +5 -2
- package/src/generators/deployment/dotnet-files/Dockerfile__tmpl__ +16 -0
- package/src/generators/deployment/frontend-files/Dockerfile__tmpl__ +10 -0
- package/src/generators/deployment/node-files/Dockerfile__tmpl__ +10 -0
- package/src/generators/deployment/node-files/__projectName__.yml__tmpl__ +12 -1
- package/src/generators/deployment/schema.d.ts +1 -0
- package/src/generators/sandbox/database-files/sandbox-postgres.yml__tmpl__ +3 -1
- package/src/generators/sandbox/files/SANDBOX.md__tmpl__ +91 -0
- package/src/generators/sandbox/sandbox.js +109 -56
- package/src/generators/sandbox/sandbox.js.map +1 -1
- package/src/generators/sandbox/sandbox.spec.ts +110 -13
- package/src/generators/sandbox/schema.d.ts +10 -0
- package/src/generators/sandbox/schema.json +13 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.d.ts +2 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.js +54 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.js.map +1 -0
- package/src/migrations/convert-sandbox-target/convert-sandbox-target.spec.ts +111 -0
- package/src/utils/app-type.d.ts +18 -0
- package/src/utils/app-type.js +43 -0
- package/src/utils/app-type.js.map +1 -0
- package/src/utils/git-utils.js +4 -2
- package/src/utils/git-utils.js.map +1 -1
- package/src/utils/oc-utils.d.ts +1 -0
- package/src/utils/oc-utils.js +21 -0
- package/src/utils/oc-utils.js.map +1 -1
- package/src/generators/deployment/dotnet-files/Dockerfile.template +0 -10
- package/src/generators/deployment/frontend-files/Dockerfile.template +0 -8
- package/src/generators/deployment/node-files/Dockerfile.template +0 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
addProjectConfiguration,
|
|
3
|
+
readNxJson,
|
|
3
4
|
readProjectConfiguration,
|
|
4
5
|
} from '@nx/devkit';
|
|
5
6
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
@@ -9,18 +10,24 @@ import generator from './sandbox';
|
|
|
9
10
|
import { Schema } from './schema';
|
|
10
11
|
|
|
11
12
|
jest.mock('../../adsp');
|
|
13
|
+
jest.mock('../../utils/oc-utils', () => ({
|
|
14
|
+
...jest.requireActual('../../utils/oc-utils'),
|
|
15
|
+
getClusterIngressDomain: jest.fn(() => 'apps.test.example.com'),
|
|
16
|
+
}));
|
|
12
17
|
const utilsMock = utils as jest.Mocked<typeof utils>;
|
|
13
18
|
utilsMock.getAdspConfiguration.mockResolvedValue({
|
|
14
19
|
tenant: 'test',
|
|
15
20
|
tenantRealm: 'test',
|
|
16
21
|
accessServiceUrl: environments.test.accessServiceUrl,
|
|
17
22
|
directoryServiceUrl: environments.test.directoryServiceUrl,
|
|
23
|
+
accessToken: 'mock-token',
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
describe('Sandbox Generator', () => {
|
|
21
27
|
const options: Schema = {
|
|
22
28
|
project: 'test',
|
|
23
29
|
sandboxProject: 'test-sandbox',
|
|
30
|
+
registry: 'ghcr.io/test-org',
|
|
24
31
|
};
|
|
25
32
|
|
|
26
33
|
function addNodeProject(host) {
|
|
@@ -36,10 +43,11 @@ describe('Sandbox Generator', () => {
|
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
function addFrontendProject(host) {
|
|
46
|
+
function addFrontendProject(host, tags?: string[]) {
|
|
40
47
|
addProjectConfiguration(host, 'test', {
|
|
41
48
|
root: 'apps/test',
|
|
42
49
|
projectType: 'application',
|
|
50
|
+
tags,
|
|
43
51
|
targets: {
|
|
44
52
|
build: {
|
|
45
53
|
executor: '@nx/webpack:webpack',
|
|
@@ -75,19 +83,75 @@ describe('Sandbox Generator', () => {
|
|
|
75
83
|
expect(manifest).not.toContain('ImageStream');
|
|
76
84
|
});
|
|
77
85
|
|
|
78
|
-
it('adds sandbox nx
|
|
86
|
+
it('adds a sandbox target wired to the @abgov/nx-oc:sandbox executor', async () => {
|
|
79
87
|
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
80
88
|
addNodeProject(host);
|
|
81
89
|
|
|
82
90
|
await generator(host, options);
|
|
83
91
|
|
|
84
92
|
const config = readProjectConfiguration(host, 'test');
|
|
85
|
-
|
|
86
|
-
expect(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
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();
|
|
105
|
+
|
|
106
|
+
// The in-cluster BuildConfig flow is gone.
|
|
107
|
+
expect(host.exists('.openshift/test/sandbox-build.yml')).toBeFalsy();
|
|
108
|
+
});
|
|
109
|
+
|
|
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');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('persists the resolved registry to nx.json for reuse', async () => {
|
|
144
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
145
|
+
addNodeProject(host);
|
|
146
|
+
|
|
147
|
+
await generator(host, options);
|
|
148
|
+
|
|
149
|
+
const nxJson = readNxJson(host);
|
|
150
|
+
expect(
|
|
151
|
+
(nxJson.generators as Record<string, { registry?: string }>)[
|
|
152
|
+
'@abgov/nx-oc:sandbox'
|
|
153
|
+
].registry
|
|
154
|
+
).toBe('ghcr.io/test-org');
|
|
91
155
|
});
|
|
92
156
|
|
|
93
157
|
it('adds sandbox-teardown nx target', async () => {
|
|
@@ -104,6 +168,12 @@ describe('Sandbox Generator', () => {
|
|
|
104
168
|
expect(cmds.some((c) => c.includes('test-sandbox'))).toBeTruthy();
|
|
105
169
|
expect(cmds.some((c) => c.includes('-l app=test'))).toBeTruthy();
|
|
106
170
|
expect(cmds.some((c) => c.includes('all,configmap'))).toBeTruthy();
|
|
171
|
+
// Teardown also removes the sandbox package (best-effort).
|
|
172
|
+
expect(
|
|
173
|
+
cmds.some((c) =>
|
|
174
|
+
c.includes('packages/container/test-sandbox-test')
|
|
175
|
+
)
|
|
176
|
+
).toBeTruthy();
|
|
107
177
|
});
|
|
108
178
|
|
|
109
179
|
it('generates shared postgres manifest with secret-backed DATABASE_URL', async () => {
|
|
@@ -121,10 +191,10 @@ describe('Sandbox Generator', () => {
|
|
|
121
191
|
expect(appManifest).toContain('sandbox-postgres-creds');
|
|
122
192
|
expect(appManifest).toContain('$(POSTGRES_PASSWORD)');
|
|
123
193
|
|
|
194
|
+
// The database type is passed to the executor, which provisions the shared
|
|
195
|
+
// instance + per-app database at deploy time.
|
|
124
196
|
const config = readProjectConfiguration(host, 'test');
|
|
125
|
-
|
|
126
|
-
expect(cmds.some((c) => c.includes('sandbox-postgres-creds'))).toBeTruthy();
|
|
127
|
-
expect(cmds.some((c) => c.includes('sandbox-postgres.yml'))).toBeTruthy();
|
|
197
|
+
expect(config.targets['sandbox'].options.database).toBe('postgres');
|
|
128
198
|
});
|
|
129
199
|
|
|
130
200
|
it('generates shared mongodb manifest with secret-backed MONGODB_URI', async () => {
|
|
@@ -143,7 +213,34 @@ describe('Sandbox Generator', () => {
|
|
|
143
213
|
expect(appManifest).toContain('$(MONGO_PASSWORD)');
|
|
144
214
|
|
|
145
215
|
const config = readProjectConfiguration(host, 'test');
|
|
146
|
-
|
|
147
|
-
|
|
216
|
+
expect(config.targets['sandbox'].options.database).toBe('mongo');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('registers the deployment Route redirect URI for a frontend client', async () => {
|
|
220
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
221
|
+
addFrontendProject(host);
|
|
222
|
+
utilsMock.addClientRedirectUris.mockClear();
|
|
223
|
+
|
|
224
|
+
await generator(host, options);
|
|
225
|
+
|
|
226
|
+
// Route host follows <app>-<namespace>.<ingressDomain>, registered against
|
|
227
|
+
// the public client urn:ads:<tenant>:<app> with the token from ADSP config.
|
|
228
|
+
expect(utilsMock.addClientRedirectUris).toHaveBeenCalledWith(
|
|
229
|
+
environments.test.accessServiceUrl,
|
|
230
|
+
'test',
|
|
231
|
+
'urn:ads:test:test',
|
|
232
|
+
['https://test-test-sandbox.apps.test.example.com/*'],
|
|
233
|
+
'mock-token'
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not register redirect URIs for a node service', async () => {
|
|
238
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
239
|
+
addNodeProject(host);
|
|
240
|
+
utilsMock.addClientRedirectUris.mockClear();
|
|
241
|
+
|
|
242
|
+
await generator(host, options);
|
|
243
|
+
|
|
244
|
+
expect(utilsMock.addClientRedirectUris).not.toHaveBeenCalled();
|
|
148
245
|
});
|
|
149
246
|
});
|
|
@@ -9,10 +9,20 @@ export interface Schema {
|
|
|
9
9
|
env?: string;
|
|
10
10
|
adsp?: AdspConfiguration;
|
|
11
11
|
accessToken?: string;
|
|
12
|
+
tenant?: string;
|
|
13
|
+
tenantRealm?: string;
|
|
14
|
+
registry?: string;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export interface NormalizedSchema extends Schema {
|
|
15
18
|
projectName: string;
|
|
16
19
|
appType: ApplicationType;
|
|
17
20
|
adsp: AdspConfiguration;
|
|
21
|
+
buildOutputPath: string;
|
|
22
|
+
registry: string;
|
|
23
|
+
registryHost: string;
|
|
24
|
+
registryOrg: string;
|
|
25
|
+
imageName: string;
|
|
26
|
+
imageRef: string;
|
|
27
|
+
sourceRepositoryUrl?: string;
|
|
18
28
|
}
|
|
@@ -43,6 +43,19 @@
|
|
|
43
43
|
"type": "string",
|
|
44
44
|
"description": "Access token for retrieving configuration from ADSP APIs.",
|
|
45
45
|
"alias": "at"
|
|
46
|
+
},
|
|
47
|
+
"tenant": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "ADSP tenant name. Looks up the realm and does a single login, skipping the interactive tenant picker."
|
|
50
|
+
},
|
|
51
|
+
"tenantRealm": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Keycloak realm UUID. Optional when --tenant is provided; overrides the realm looked up from the tenant service."
|
|
54
|
+
},
|
|
55
|
+
"registry": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Container registry to publish the sandbox image to (e.g., ghcr.io/my-org). Defaults to the value stored in nx.json, then derived from the git remote. Persisted to nx.json on first use.",
|
|
58
|
+
"alias": "r"
|
|
46
59
|
}
|
|
47
60
|
},
|
|
48
61
|
"required": ["project", "sandboxProject"],
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ProjectConfiguration } from '@nx/devkit';
|
|
2
|
+
import { ApplicationType } from '../generators/deployment/schema';
|
|
3
|
+
/**
|
|
4
|
+
* Detect the application type from the project's build executor.
|
|
5
|
+
*
|
|
6
|
+
* Shared by the deployment and sandbox generators so they stay in sync as new
|
|
7
|
+
* Nx build executors appear (e.g. Vite/Rspack for frontends).
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectApplicationType(config: ProjectConfiguration): ApplicationType | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The build output directory for a project, relative to the workspace root.
|
|
12
|
+
*
|
|
13
|
+
* Read from the project's `build` target so the generated Dockerfile mirrors
|
|
14
|
+
* the workspace's actual layout (e.g. `dist/apps/<project>` or
|
|
15
|
+
* `packages/<project>/dist`) instead of assuming a fixed path. Falls back to
|
|
16
|
+
* `dist/<projectRoot>`, the Nx default, when the target declares no outputPath.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getBuildOutputPath(config: ProjectConfiguration): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectApplicationType = detectApplicationType;
|
|
4
|
+
exports.getBuildOutputPath = getBuildOutputPath;
|
|
5
|
+
/**
|
|
6
|
+
* Detect the application type from the project's build executor.
|
|
7
|
+
*
|
|
8
|
+
* Shared by the deployment and sandbox generators so they stay in sync as new
|
|
9
|
+
* Nx build executors appear (e.g. Vite/Rspack for frontends).
|
|
10
|
+
*/
|
|
11
|
+
function detectApplicationType(config) {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
const build = (_a = config.targets) === null || _a === void 0 ? void 0 : _a.build;
|
|
14
|
+
switch (build === null || build === void 0 ? void 0 : build.executor) {
|
|
15
|
+
case '@nx/web:webpack':
|
|
16
|
+
case '@angular-devkit/build-angular:browser':
|
|
17
|
+
case '@nx/vite:build':
|
|
18
|
+
case '@nx/rspack:rspack':
|
|
19
|
+
return 'frontend';
|
|
20
|
+
case '@nx/node:build':
|
|
21
|
+
return 'node';
|
|
22
|
+
case '@nx-dotnet/core:build':
|
|
23
|
+
return 'dotnet';
|
|
24
|
+
case '@nx/webpack:webpack':
|
|
25
|
+
// The generic webpack executor builds both node and browser apps.
|
|
26
|
+
return ((_b = build.options) === null || _b === void 0 ? void 0 : _b.target) === 'node' ? 'node' : 'frontend';
|
|
27
|
+
default:
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The build output directory for a project, relative to the workspace root.
|
|
33
|
+
*
|
|
34
|
+
* Read from the project's `build` target so the generated Dockerfile mirrors
|
|
35
|
+
* the workspace's actual layout (e.g. `dist/apps/<project>` or
|
|
36
|
+
* `packages/<project>/dist`) instead of assuming a fixed path. Falls back to
|
|
37
|
+
* `dist/<projectRoot>`, the Nx default, when the target declares no outputPath.
|
|
38
|
+
*/
|
|
39
|
+
function getBuildOutputPath(config) {
|
|
40
|
+
var _a, _b, _c, _d;
|
|
41
|
+
return (_d = (_c = (_b = (_a = config.targets) === null || _a === void 0 ? void 0 : _a.build) === null || _b === void 0 ? void 0 : _b.options) === null || _c === void 0 ? void 0 : _c.outputPath) !== null && _d !== void 0 ? _d : `dist/${config.root}`;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=app-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-type.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/app-type.ts"],"names":[],"mappings":";;AASA,sDAoBC;AAUD,gDAEC;AAtCD;;;;;GAKG;AACH,SAAgB,qBAAqB,CACnC,MAA4B;;IAE5B,MAAM,KAAK,GAAG,MAAA,MAAM,CAAC,OAAO,0CAAE,KAAK,CAAC;IACpC,QAAQ,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,QAAQ,EAAE,CAAC;QACxB,KAAK,iBAAiB,CAAC;QACvB,KAAK,uCAAuC,CAAC;QAC7C,KAAK,gBAAgB,CAAC;QACtB,KAAK,mBAAmB;YACtB,OAAO,UAAU,CAAC;QACpB,KAAK,gBAAgB;YACnB,OAAO,MAAM,CAAC;QAChB,KAAK,uBAAuB;YAC1B,OAAO,QAAQ,CAAC;QAClB,KAAK,qBAAqB;YACxB,kEAAkE;YAClE,OAAO,CAAA,MAAA,KAAK,CAAC,OAAO,0CAAE,MAAM,MAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QAChE;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAC,MAA4B;;IAC7D,OAAO,MAAA,MAAA,MAAA,MAAA,MAAM,CAAC,OAAO,0CAAE,KAAK,0CAAE,OAAO,0CAAE,UAAU,mCAAI,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;AAC7E,CAAC"}
|
package/src/utils/git-utils.js
CHANGED
|
@@ -9,8 +9,10 @@ function getGitRemoteUrl() {
|
|
|
9
9
|
const stdout = (0, child_process_1.execSync)("git config --get remote.origin.url", { stdio: "pipe" }).toString();
|
|
10
10
|
return stdout;
|
|
11
11
|
}
|
|
12
|
-
catch (
|
|
13
|
-
|
|
12
|
+
catch (_a) {
|
|
13
|
+
// No 'origin' remote (e.g. a freshly-created workspace) — not an error;
|
|
14
|
+
// sourceRepositoryUrl is simply left unset.
|
|
15
|
+
return undefined;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
// Parses a GitHub remote URL and returns the ghcr.io registry for the org.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/git-utils.ts"],"names":[],"mappings":";;AAEA,
|
|
1
|
+
{"version":3,"file":"git-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/git-utils.ts"],"names":[],"mappings":";;AAEA,0CAaC;AAKD,4DAOC;AAGD,sCAMC;AApCD,iDAAwC;AAExC,SAAgB,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,wBAAQ,EACrB,oCAAoC,EACpC,EAAE,KAAK,EAAE,MAAM,EAAE,CAClB,CAAC,QAAQ,EAAE,CAAA;QAEZ,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,WAAM,CAAC;QACP,wEAAwE;QACxE,4CAA4C;QAC5C,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,2DAA2D;AAC3D,6CAA6C;AAC7C,SAAgB,wBAAwB,CAAC,SAAkB;;IACzD,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAK,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,MAAA,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAG,CAAC,CAAC,mCAAI,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAG,CAAC,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,qEAAqE;AACrE,SAAgB,aAAa,CAAC,SAAkB;;IAC9C,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAC3E,MAAM,QAAQ,GAAK,GAAG,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACpE,OAAO,MAAA,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAG,CAAC,CAAC,mCAAI,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAG,CAAC,CAAC,CAAC;AAC1C,CAAC"}
|
package/src/utils/oc-utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function getOcServerUrl(): string | undefined;
|
|
2
|
+
export declare function getClusterIngressDomain(): string | undefined;
|
|
2
3
|
export declare function getSaToken(saName: string, namespace: string): string | undefined;
|
|
3
4
|
export declare function createDockerRegistrySecret(name: string, server: string, username: string, password: string, namespace: string): boolean;
|
|
4
5
|
export declare function linkSecretToServiceAccount(secretName: string, saName: string, namespace: string): boolean;
|
package/src/utils/oc-utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getOcServerUrl = getOcServerUrl;
|
|
4
|
+
exports.getClusterIngressDomain = getClusterIngressDomain;
|
|
4
5
|
exports.getSaToken = getSaToken;
|
|
5
6
|
exports.createDockerRegistrySecret = createDockerRegistrySecret;
|
|
6
7
|
exports.linkSecretToServiceAccount = linkSecretToServiceAccount;
|
|
@@ -15,6 +16,26 @@ function getOcServerUrl() {
|
|
|
15
16
|
return undefined;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
// Derives the cluster's ingress (apps) domain from the console URL, e.g.
|
|
20
|
+
// https://console-openshift-console.apps.example.com -> apps.example.com.
|
|
21
|
+
// Used to predict a deployment's default Route host by convention. Readable by
|
|
22
|
+
// any logged-in user (no cluster-scoped permissions needed).
|
|
23
|
+
function getClusterIngressDomain() {
|
|
24
|
+
try {
|
|
25
|
+
const consoleUrl = (0, child_process_1.execFileSync)('oc', ['whoami', '--show-console'], {
|
|
26
|
+
stdio: 'pipe',
|
|
27
|
+
})
|
|
28
|
+
.toString()
|
|
29
|
+
.trim();
|
|
30
|
+
if (!consoleUrl)
|
|
31
|
+
return undefined;
|
|
32
|
+
const host = new URL(consoleUrl).host;
|
|
33
|
+
return host.replace(/^console-openshift-console\./, '') || undefined;
|
|
34
|
+
}
|
|
35
|
+
catch (_a) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
18
39
|
// Creates a bounded SA token valid for one year (OCP 4.11+ TokenRequest API).
|
|
19
40
|
// Falls back to the legacy `oc sa get-token` on older clusters.
|
|
20
41
|
function getSaToken(saName, namespace) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oc-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/oc-utils.ts"],"names":[],"mappings":";;AAEA,wCAUC;AAID,gCAgBC;AAED,gEAmBC;AAED,gEAWC;AAED,oCAiBC;AAED,sCAmCC;
|
|
1
|
+
{"version":3,"file":"oc-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/oc-utils.ts"],"names":[],"mappings":"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"}
|