@abgov/nx-oc 13.0.0-beta.1 → 13.0.0-beta.2
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/package.json +1 -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/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/sandbox.js +151 -35
- package/src/generators/sandbox/sandbox.js.map +1 -1
- package/src/generators/sandbox/sandbox.spec.ts +139 -2
- package/src/generators/sandbox/schema.d.ts +10 -0
- package/src/generators/sandbox/schema.json +13 -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
package/package.json
CHANGED
package/src/adsp/adsp-utils.d.ts
CHANGED
|
@@ -15,3 +15,13 @@ export declare function getAdspConfiguration(_host: Tree, options: {
|
|
|
15
15
|
tenantRealm?: string;
|
|
16
16
|
}): Promise<AdspConfiguration>;
|
|
17
17
|
export declare function isAdspOptions(options: unknown): options is AdspOptions;
|
|
18
|
+
/**
|
|
19
|
+
* Registers redirect URIs (and matching post-logout URIs) on an existing public
|
|
20
|
+
* client so a deployed app can complete browser sign-in/out against its route.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent and best-effort: no-ops without a token, skips if the client isn't
|
|
23
|
+
* found, and logs (never throws) on failure so it can't break generation. The
|
|
24
|
+
* client's `webOrigins` is intentionally left as-is — the default `["+"]`
|
|
25
|
+
* already allows CORS from any redirect-URI origin.
|
|
26
|
+
*/
|
|
27
|
+
export declare function addClientRedirectUris(accessServiceUrl: string, realm: string, clientId: string, redirectUris: string[], accessToken: string | undefined): Promise<void>;
|
package/src/adsp/adsp-utils.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.getServiceUrls = getServiceUrls;
|
|
|
6
6
|
exports.selectTenant = selectTenant;
|
|
7
7
|
exports.getAdspConfiguration = getAdspConfiguration;
|
|
8
8
|
exports.isAdspOptions = isAdspOptions;
|
|
9
|
+
exports.addClientRedirectUris = addClientRedirectUris;
|
|
9
10
|
const tslib_1 = require("tslib");
|
|
10
11
|
const devkit_1 = require("@nx/devkit");
|
|
11
12
|
const axios_1 = require("axios");
|
|
@@ -162,4 +163,51 @@ function isAdspOptions(options) {
|
|
|
162
163
|
var _a;
|
|
163
164
|
return !!((_a = options === null || options === void 0 ? void 0 : options.adsp) === null || _a === void 0 ? void 0 : _a.tenantRealm);
|
|
164
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Registers redirect URIs (and matching post-logout URIs) on an existing public
|
|
168
|
+
* client so a deployed app can complete browser sign-in/out against its route.
|
|
169
|
+
*
|
|
170
|
+
* Idempotent and best-effort: no-ops without a token, skips if the client isn't
|
|
171
|
+
* found, and logs (never throws) on failure so it can't break generation. The
|
|
172
|
+
* client's `webOrigins` is intentionally left as-is — the default `["+"]`
|
|
173
|
+
* already allows CORS from any redirect-URI origin.
|
|
174
|
+
*/
|
|
175
|
+
function addClientRedirectUris(accessServiceUrl, realm, clientId, redirectUris, accessToken) {
|
|
176
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
178
|
+
if (!accessToken || redirectUris.length === 0)
|
|
179
|
+
return;
|
|
180
|
+
const base = new URL(`/auth/admin/realms/${realm}/clients`, accessServiceUrl)
|
|
181
|
+
.href;
|
|
182
|
+
const authHeader = { Authorization: `Bearer ${accessToken}` };
|
|
183
|
+
const union = (existing, additions) => Array.from(new Set([...(existing !== null && existing !== void 0 ? existing : []), ...additions]));
|
|
184
|
+
try {
|
|
185
|
+
const { data: clients } = yield axios_1.default.get(base, {
|
|
186
|
+
params: { clientId },
|
|
187
|
+
headers: authHeader,
|
|
188
|
+
});
|
|
189
|
+
const client = clients === null || clients === void 0 ? void 0 : clients[0];
|
|
190
|
+
if (!client) {
|
|
191
|
+
console.log(`[nx-oc] Public client '${clientId}' not found in realm '${realm}' — skipping redirect URI update.`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const existingPostLogout = ((_b = (_a = client.attributes) === null || _a === void 0 ? void 0 : _a['post.logout.redirect.uris']) !== null && _b !== void 0 ? _b : '')
|
|
195
|
+
.split('##')
|
|
196
|
+
.filter(Boolean);
|
|
197
|
+
const nextRedirects = union(client.redirectUris, redirectUris);
|
|
198
|
+
const nextPostLogout = union(existingPostLogout, redirectUris).join('##');
|
|
199
|
+
if (nextRedirects.length === ((_d = (_c = client.redirectUris) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) &&
|
|
200
|
+
nextPostLogout === ((_f = (_e = client.attributes) === null || _e === void 0 ? void 0 : _e['post.logout.redirect.uris']) !== null && _f !== void 0 ? _f : '')) {
|
|
201
|
+
console.log(`[nx-oc] Client '${clientId}' already allows ${redirectUris.join(', ')}.`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
yield axios_1.default.put(`${base}/${client.id}`, Object.assign(Object.assign({}, client), { redirectUris: nextRedirects, attributes: Object.assign(Object.assign({}, client.attributes), { 'post.logout.redirect.uris': nextPostLogout }) }), { headers: authHeader });
|
|
205
|
+
console.log(`[nx-oc] Registered redirect URI(s) on client '${clientId}': ${redirectUris.join(', ')}`);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const detail = (_j = (_h = (_g = err === null || err === void 0 ? void 0 : err.response) === null || _g === void 0 ? void 0 : _g.status) !== null && _h !== void 0 ? _h : err === null || err === void 0 ? void 0 : err.message) !== null && _j !== void 0 ? _j : err;
|
|
209
|
+
console.log(`[nx-oc] Could not update redirect URIs for '${clientId}': ${detail}`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
165
213
|
//# sourceMappingURL=adsp-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adsp-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/adsp/adsp-utils.ts"],"names":[],"mappings":";;AAeA,sCAOC;AAED,gCAqFC;AAED,wCAYC;AAED,oCAoBC;AAED,oDAgDC;AAED,sCAEC;;
|
|
1
|
+
{"version":3,"file":"adsp-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/adsp/adsp-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
// minimatch is always present (nx depends on it); used only in this test.
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
5
|
+
import minimatch = require('minimatch');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Guards the source -> published-package boundary.
|
|
9
|
+
*
|
|
10
|
+
* The build copies non-source files (generator templates, schemas, static
|
|
11
|
+
* assets) into the package via the build target's `assets` globs. Any such file
|
|
12
|
+
* NOT matched by a glob is silently dropped from the published package, so a
|
|
13
|
+
* consumer's generate produces output referencing a file that was never
|
|
14
|
+
* shipped. This is exactly how `Dockerfile__tmpl__` was lost: the rename from
|
|
15
|
+
* `Dockerfile.template` (dotted) to `Dockerfile__tmpl__` (dotless) stopped it
|
|
16
|
+
* matching `**\/*.!(ts)`.
|
|
17
|
+
*
|
|
18
|
+
* Unit tests of the generators can't catch this — they resolve templates from
|
|
19
|
+
* the source tree, never the packaged output. This asserts the invariant
|
|
20
|
+
* directly: every non-TypeScript file under src is covered by an asset glob.
|
|
21
|
+
*/
|
|
22
|
+
describe('build assets packaging', () => {
|
|
23
|
+
const srcRoot = __dirname;
|
|
24
|
+
const projectRoot = path.join(srcRoot, '..');
|
|
25
|
+
const repoRoot = path.join(projectRoot, '..', '..');
|
|
26
|
+
|
|
27
|
+
function listFiles(dir: string): string[] {
|
|
28
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
29
|
+
const full = path.join(dir, entry.name);
|
|
30
|
+
return entry.isDirectory() ? listFiles(full) : [full];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('matches every non-TypeScript src file with an asset glob', () => {
|
|
35
|
+
const project = JSON.parse(
|
|
36
|
+
fs.readFileSync(path.join(projectRoot, 'project.json'), 'utf-8')
|
|
37
|
+
);
|
|
38
|
+
const assets: unknown[] = project.targets.build.options.assets ?? [];
|
|
39
|
+
|
|
40
|
+
// Globs whose input is this package's src directory.
|
|
41
|
+
const srcGlobs = assets
|
|
42
|
+
.filter(
|
|
43
|
+
(a): a is { input: string; glob: string } =>
|
|
44
|
+
typeof a === 'object' &&
|
|
45
|
+
a !== null &&
|
|
46
|
+
'input' in a &&
|
|
47
|
+
path.resolve(repoRoot, (a as { input: string }).input) === srcRoot
|
|
48
|
+
)
|
|
49
|
+
.map((a) => a.glob);
|
|
50
|
+
|
|
51
|
+
// Every non-source file under src must ship (tsc only emits .ts -> .js).
|
|
52
|
+
const mustShip = listFiles(srcRoot)
|
|
53
|
+
.filter((f) => !/\.tsx?$/.test(f))
|
|
54
|
+
.map((f) => path.relative(srcRoot, f));
|
|
55
|
+
|
|
56
|
+
const unmatched = mustShip.filter(
|
|
57
|
+
(rel) => !srcGlobs.some((glob) => minimatch(rel, glob, { dot: true }))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(unmatched).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -7,51 +7,37 @@ const path = require("path");
|
|
|
7
7
|
const yaml = require("yaml");
|
|
8
8
|
const pipeline_envs_1 = require("../../pipeline-envs");
|
|
9
9
|
const git_utils_1 = require("../../utils/git-utils");
|
|
10
|
+
const app_type_1 = require("../../utils/app-type");
|
|
10
11
|
const adsp_1 = require("../../adsp");
|
|
11
12
|
const infraManifestFile = '.openshift/environments.yml';
|
|
12
13
|
function normalizeOptions(host, options) {
|
|
13
14
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
14
|
-
var _a, _b, _c, _d;
|
|
15
|
+
var _a, _b, _c, _d, _e;
|
|
15
16
|
const projectName = (0, devkit_1.names)(options.project).fileName;
|
|
16
17
|
const result = host.read(infraManifestFile).toString();
|
|
17
18
|
const { items } = yaml.parse(result);
|
|
18
19
|
const ocInfraProject = ((_b = (_a = items[0]) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.namespace) || '';
|
|
19
20
|
const SA_PREFIX = 'system:serviceaccounts:';
|
|
20
21
|
const ocEnvProjects = (_d = (_c = items[0]) === null || _c === void 0 ? void 0 : _c.subjects) === null || _d === void 0 ? void 0 : _d.filter((s) => s.kind === 'Group' && s.name.startsWith(SA_PREFIX)).map((s) => s.name.replace(SA_PREFIX, ''));
|
|
21
|
-
// TODO: Find a better way to determine this.
|
|
22
22
|
const config = (0, devkit_1.readProjectConfiguration)(host, projectName);
|
|
23
|
-
|
|
24
|
-
if (!appType) {
|
|
25
|
-
switch (config.targets.build.executor) {
|
|
26
|
-
case '@nx/web:webpack':
|
|
27
|
-
case '@angular-devkit/build-angular:browser':
|
|
28
|
-
appType = 'frontend';
|
|
29
|
-
break;
|
|
30
|
-
case '@nx/node:build':
|
|
31
|
-
appType = 'node';
|
|
32
|
-
break;
|
|
33
|
-
case '@nx-dotnet/core:build':
|
|
34
|
-
appType = 'dotnet';
|
|
35
|
-
break;
|
|
36
|
-
case '@nx/webpack:webpack': {
|
|
37
|
-
// More recent version of NX switched to use a generic webpack executor for builds.
|
|
38
|
-
appType =
|
|
39
|
-
config.targets.build.options.target === 'node' ? 'node' : 'frontend';
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
23
|
+
const appType = (_e = options.appType) !== null && _e !== void 0 ? _e : (0, app_type_1.detectApplicationType)(config);
|
|
44
24
|
const adsp = yield (0, adsp_1.getAdspConfiguration)(host, options);
|
|
45
25
|
return Object.assign(Object.assign({}, options), { appType,
|
|
46
26
|
adsp,
|
|
47
27
|
projectName,
|
|
48
28
|
ocInfraProject,
|
|
49
|
-
ocEnvProjects });
|
|
29
|
+
ocEnvProjects, buildOutputPath: (0, app_type_1.getBuildOutputPath)(config) });
|
|
50
30
|
});
|
|
51
31
|
}
|
|
52
32
|
function addFiles(host, options) {
|
|
53
33
|
var _a, _b;
|
|
54
|
-
const templateOptions = Object.assign(Object.assign(Object.assign({}, options), options.adsp), {
|
|
34
|
+
const templateOptions = Object.assign(Object.assign(Object.assign({}, options), options.adsp), {
|
|
35
|
+
// Clean https URL for the image's source label (never the raw remote, which
|
|
36
|
+
// may be an SSH URL or carry a trailing newline).
|
|
37
|
+
sourceRepositoryUrl: (() => {
|
|
38
|
+
const repo = (0, git_utils_1.getGitHubRepo)((0, git_utils_1.getGitRemoteUrl)());
|
|
39
|
+
return repo ? `https://github.com/${repo}` : '';
|
|
40
|
+
})(), database: (_a = options.database) !== null && _a !== void 0 ? _a : 'none', sandbox: (_b = options.sandbox) !== null && _b !== void 0 ? _b : false, tmpl: '' });
|
|
55
41
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, `${options.appType}-files`), `./.openshift/${options.projectName}`, templateOptions);
|
|
56
42
|
}
|
|
57
43
|
function default_1(host, options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/deployment/deployment.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"deployment.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/deployment/deployment.ts"],"names":[],"mappings":";;AAuEA,4BAqCC;;AA5GD,uCAOoB;AACpB,6BAA6B;AAC7B,6BAA6B;AAC7B,uDAA2D;AAC3D,qDAAuE;AACvE,mDAAiF;AAEjF,qCAAkD;AAElD,MAAM,iBAAiB,GAAG,6BAA6B,CAAC;AAExD,SAAe,gBAAgB,CAC7B,IAAU,EACV,OAAe;;;QAEf,MAAM,WAAW,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;QAEpD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvD,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,cAAc,GAAG,CAAA,MAAA,MAAA,KAAK,CAAC,CAAC,CAAC,0CAAE,QAAQ,0CAAE,SAAS,KAAI,EAAE,CAAC;QAE3D,MAAM,SAAS,GAAG,yBAAyB,CAAC;QAC5C,MAAM,aAAa,GAAG,MAAA,MAAA,KAAK,CAAC,CAAC,CAAC,0CAAE,QAAQ,0CACpC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EACjE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QAE7C,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,EAAE,OAAO,CAAC,CAAC;QAEvD,uCACK,OAAO,KACV,OAAO;YACP,IAAI;YACJ,WAAW;YACX,cAAc;YACd,aAAa,EACb,eAAe,EAAE,IAAA,6BAAkB,EAAC,MAAM,CAAC,IAC3C;IACJ,CAAC;CAAA;AAED,SAAS,QAAQ,CAAC,IAAU,EAAE,OAAyB;;IACrD,MAAM,eAAe,iDAChB,OAAO,GACP,OAAO,CAAC,IAAI;QACf,4EAA4E;QAC5E,kDAAkD;QAClD,mBAAmB,EAAE,CAAC,GAAG,EAAE;YACzB,MAAM,IAAI,GAAG,IAAA,yBAAa,EAAC,IAAA,2BAAe,GAAE,CAAC,CAAC;YAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,CAAC,CAAC,EAAE,EACJ,QAAQ,EAAE,MAAA,OAAO,CAAC,QAAQ,mCAAI,MAAM,EACpC,OAAO,EAAE,MAAA,OAAO,CAAC,OAAO,mCAAI,KAAK,EACjC,IAAI,EAAE,EAAE,GACT,CAAC;IACF,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,EAChD,gBAAgB,OAAO,CAAC,WAAW,EAAE,EACrC,eAAe,CAChB,CAAC;AACJ,CAAC;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,MAAM,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/D,IAAI,MAAM,CAAC,WAAW,KAAK,aAAa,EAAE,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;YACvD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACpC,OAAO,CAAC,GAAG,CACT,qEAAqE,CACtE,CAAC;YACF,OAAO;QACT,CAAC;QAED,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,sDAAsD,CAAC,CAAC;YACpE,OAAO;QACT,CAAC;QAED,MAAM,CAAC,OAAO,mCACT,MAAM,CAAC,OAAO,KACjB,YAAY,EAAE;gBACZ,QAAQ,EAAE,oBAAoB;gBAC9B,OAAO,EAAE;oBACP,SAAS,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE;;wBAAC,OAAA,CAAC;4BAC9D,OAAO;4BACP,GAAG,EAAE,MAAA,4BAAI,CAAC,CAAC,CAAC,0CAAE,WAAW,EAAE;yBAC5B,CAAC,CAAA;qBAAA,CAAC;iBACJ;aACF,GACF,CAAC;QAEF,IAAA,mCAA0B,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAE1D,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAClC,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}
|
|
@@ -158,6 +158,10 @@ describe('Deployment Generator', () => {
|
|
|
158
158
|
const manifest = host.read('.openshift/test/test.yml').toString();
|
|
159
159
|
expect(manifest).toContain('readinessProbe');
|
|
160
160
|
expect(manifest).toContain('livenessProbe');
|
|
161
|
+
// The service authenticates with ADSP using a confidential client secret,
|
|
162
|
+
// injected from the ${APP_NAME}-secrets Secret.
|
|
163
|
+
expect(manifest).toContain('CLIENT_SECRET');
|
|
164
|
+
expect(manifest).toContain('${APP_NAME}-secrets');
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
it('can generate deployment for dotnet', async () => {
|
|
@@ -218,8 +222,7 @@ describe('Deployment Generator', () => {
|
|
|
218
222
|
const manifest = host.read('.openshift/test/test.yml').toString();
|
|
219
223
|
expect(manifest).toContain('DATABASE_URL');
|
|
220
224
|
expect(manifest).toContain('initContainers');
|
|
221
|
-
expect(manifest).toContain('
|
|
222
|
-
expect(manifest).toContain('migrate');
|
|
225
|
+
expect(manifest).toContain('migrate.js');
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
it('includes MONGODB_URI secretKeyRef without init container for mongo node deployment', async () => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
FROM registry.access.redhat.com/ubi8/dotnet-80 AS build
|
|
2
|
+
ARG PROJECT
|
|
3
|
+
WORKDIR /src
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN dotnet publish ${PROJECT} -c Release -o /app/publish
|
|
6
|
+
|
|
7
|
+
FROM registry.access.redhat.com/ubi8/dotnet-80-runtime
|
|
8
|
+
<% if (sourceRepositoryUrl) { %>
|
|
9
|
+
# Provenance: associate the image with its source repo (shown in GHCR).
|
|
10
|
+
LABEL org.opencontainers.image.source=<%= sourceRepositoryUrl %>
|
|
11
|
+
<% } %>
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY --from=build /app/publish .
|
|
14
|
+
ENV ASPNETCORE_URLS=http://+:5000
|
|
15
|
+
EXPOSE 5000
|
|
16
|
+
CMD ["dotnet", "<%= projectName %>.dll"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
FROM registry.access.redhat.com/ubi9/nginx-120
|
|
2
|
+
<% if (sourceRepositoryUrl) { %>
|
|
3
|
+
# Provenance: associate the image with its source repo (shown in GHCR).
|
|
4
|
+
LABEL org.opencontainers.image.source=<%= sourceRepositoryUrl %>
|
|
5
|
+
<% } %>
|
|
6
|
+
# Build output path mirrors the workspace layout (from the project's build target).
|
|
7
|
+
COPY ./<%= buildOutputPath %>/nginx.conf "${NGINX_CONF_PATH}"
|
|
8
|
+
COPY ./<%= buildOutputPath %> .
|
|
9
|
+
|
|
10
|
+
CMD nginx -g "daemon off;"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
FROM registry.access.redhat.com/ubi9/nodejs-24
|
|
2
|
+
<% if (sourceRepositoryUrl) { %>
|
|
3
|
+
# Provenance: associate the image with its source repo (shown in GHCR).
|
|
4
|
+
LABEL org.opencontainers.image.source=<%= sourceRepositoryUrl %>
|
|
5
|
+
<% } %>
|
|
6
|
+
# Build output path mirrors the workspace layout (from the project's build target).
|
|
7
|
+
COPY ./<%= buildOutputPath %> .
|
|
8
|
+
COPY ./node_modules ./node_modules
|
|
9
|
+
|
|
10
|
+
CMD node main.js
|
|
@@ -91,7 +91,7 @@ objects:
|
|
|
91
91
|
<% } else { %>
|
|
92
92
|
image: image-registry.openshift-image-registry.svc:5000/${INFRA_PROJECT}/${APP_NAME}:${DEPLOY_TAG}
|
|
93
93
|
<% } %>
|
|
94
|
-
command: ["
|
|
94
|
+
command: ["node", "migrate.js"]
|
|
95
95
|
envFrom:
|
|
96
96
|
- configMapRef:
|
|
97
97
|
name: ${APP_NAME}
|
|
@@ -127,6 +127,17 @@ objects:
|
|
|
127
127
|
value: '3333'
|
|
128
128
|
- name: LOG_LEVEL
|
|
129
129
|
value: info
|
|
130
|
+
# ADSP confidential-client secret the service uses to authenticate
|
|
131
|
+
# with the access service. Sandbox: created by `nx run <app>:sandbox`
|
|
132
|
+
# from the service's .env.
|
|
133
|
+
<% if (!sandbox) { %>
|
|
134
|
+
# Other envs: oc create secret generic ${APP_NAME}-secrets --from-literal=CLIENT_SECRET=<secret>
|
|
135
|
+
<% } %>
|
|
136
|
+
- name: CLIENT_SECRET
|
|
137
|
+
valueFrom:
|
|
138
|
+
secretKeyRef:
|
|
139
|
+
name: ${APP_NAME}-secrets
|
|
140
|
+
key: CLIENT_SECRET
|
|
130
141
|
<% if (database === 'postgres') { %>
|
|
131
142
|
<% if (sandbox) { %>
|
|
132
143
|
- name: POSTGRES_PASSWORD
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
# deleted on re-apply, so data persists across deployments.
|
|
4
4
|
#
|
|
5
5
|
# Multiple apps in the same namespace share this instance. Each app uses its
|
|
6
|
-
# own database (<appName>_sandbox)
|
|
6
|
+
# own database (<appName>_sandbox): the `nx run <app>:sandbox` target creates it
|
|
7
|
+
# (idempotently) after Postgres is ready, and the app's migrate init container
|
|
8
|
+
# applies the schema on deploy.
|
|
7
9
|
#
|
|
8
10
|
# Credentials are stored in the sandbox-postgres-creds Secret, created once
|
|
9
11
|
# per namespace by `nx run <app>:sandbox` on first use. Subsequent apps read
|
|
@@ -6,39 +6,68 @@ const devkit_1 = require("@nx/devkit");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const adsp_1 = require("../../adsp");
|
|
8
8
|
const git_utils_1 = require("../../utils/git-utils");
|
|
9
|
+
const oc_utils_1 = require("../../utils/oc-utils");
|
|
10
|
+
const app_type_1 = require("../../utils/app-type");
|
|
11
|
+
const SANDBOX_GENERATOR = '@abgov/nx-oc:sandbox';
|
|
12
|
+
// Resolves the sandbox container registry once per workspace and persists it to
|
|
13
|
+
// nx.json so subsequent sandbox generations reuse it without re-prompting:
|
|
14
|
+
// --registry flag → nx.json → derived from git remote (ghcr.io/<org>) → prompt.
|
|
15
|
+
function resolveRegistry(host, registry, remoteUrl) {
|
|
16
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
var _a, _b, _c;
|
|
18
|
+
if (registry)
|
|
19
|
+
return persistRegistry(host, registry);
|
|
20
|
+
const stored = (_c = (_b = (_a = (0, devkit_1.readNxJson)(host)) === null || _a === void 0 ? void 0 : _a.generators) === null || _b === void 0 ? void 0 : _b[SANDBOX_GENERATOR]) === null || _c === void 0 ? void 0 : _c.registry;
|
|
21
|
+
if (stored)
|
|
22
|
+
return stored;
|
|
23
|
+
const derived = (0, git_utils_1.deriveRegistryFromRemote)(remoteUrl);
|
|
24
|
+
if (derived) {
|
|
25
|
+
console.log(`\n✓ Sandbox registry: ${derived.toLowerCase()} (derived from git remote)\n`);
|
|
26
|
+
return persistRegistry(host, derived);
|
|
27
|
+
}
|
|
28
|
+
const { prompt } = yield Promise.resolve().then(() => require('enquirer'));
|
|
29
|
+
const { registry: answered } = yield prompt({
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'registry',
|
|
32
|
+
message: 'What container registry should sandbox images be published to (e.g., ghcr.io/my-org)?',
|
|
33
|
+
});
|
|
34
|
+
return persistRegistry(host, answered);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Container registries (GHCR) require lowercase paths, so normalize on store.
|
|
38
|
+
function persistRegistry(host, registry) {
|
|
39
|
+
var _a, _b, _c;
|
|
40
|
+
const value = registry.toLowerCase();
|
|
41
|
+
const nxJson = (_a = (0, devkit_1.readNxJson)(host)) !== null && _a !== void 0 ? _a : {};
|
|
42
|
+
const generators = ((_b = nxJson.generators) !== null && _b !== void 0 ? _b : {});
|
|
43
|
+
generators[SANDBOX_GENERATOR] = Object.assign(Object.assign({}, ((_c = generators[SANDBOX_GENERATOR]) !== null && _c !== void 0 ? _c : {})), { registry: value });
|
|
44
|
+
nxJson.generators = generators;
|
|
45
|
+
(0, devkit_1.updateNxJson)(host, nxJson);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
9
48
|
function normalizeOptions(host, options) {
|
|
10
49
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
11
|
-
var _a;
|
|
50
|
+
var _a, _b;
|
|
12
51
|
const projectName = (0, devkit_1.names)(options.project).fileName;
|
|
13
52
|
const config = (0, devkit_1.readProjectConfiguration)(host, projectName);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
appType = 'node';
|
|
23
|
-
break;
|
|
24
|
-
case '@nx-dotnet/core:build':
|
|
25
|
-
appType = 'dotnet';
|
|
26
|
-
break;
|
|
27
|
-
case '@nx/webpack:webpack':
|
|
28
|
-
appType =
|
|
29
|
-
config.targets.build.options.target === 'node' ? 'node' : 'frontend';
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const adsp = yield (0, adsp_1.getAdspConfiguration)(host, Object.assign(Object.assign({}, options), { env: (_a = options.env) !== null && _a !== void 0 ? _a : 'dev' }));
|
|
53
|
+
const appType = (_a = options.appType) !== null && _a !== void 0 ? _a : (0, app_type_1.detectApplicationType)(config);
|
|
54
|
+
const adsp = yield (0, adsp_1.getAdspConfiguration)(host, Object.assign(Object.assign({}, options), { env: (_b = options.env) !== null && _b !== void 0 ? _b : 'dev' }));
|
|
55
|
+
const remoteUrl = (0, git_utils_1.getGitRemoteUrl)();
|
|
56
|
+
const registry = (yield resolveRegistry(host, options.registry, remoteUrl)).toLowerCase();
|
|
57
|
+
// Prefix the image with the (per-user) sandbox namespace so images from
|
|
58
|
+
// different experimenters never collide on GHCR's org-global package names.
|
|
59
|
+
const imageName = `${options.sandboxProject}-${projectName}`.toLowerCase();
|
|
60
|
+
const repoSlug = (0, git_utils_1.getGitHubRepo)(remoteUrl);
|
|
34
61
|
return Object.assign(Object.assign({}, options), { appType,
|
|
35
62
|
adsp,
|
|
36
|
-
projectName });
|
|
63
|
+
projectName, buildOutputPath: (0, app_type_1.getBuildOutputPath)(config), registry, registryHost: registry.split('/')[0], registryOrg: registry.split('/').slice(1).join('/'), imageName, imageRef: `${registry}/${imageName}:sandbox`, sourceRepositoryUrl: repoSlug ? `https://github.com/${repoSlug}` : undefined });
|
|
37
64
|
});
|
|
38
65
|
}
|
|
39
66
|
function addManifestFiles(host, options) {
|
|
40
|
-
var _a;
|
|
41
|
-
const templateOptions = Object.assign(Object.assign(Object.assign({}, options), options.adsp), {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
const templateOptions = Object.assign(Object.assign(Object.assign({}, options), options.adsp), {
|
|
69
|
+
// Clean https URL for the image's source label (provenance / UI connect).
|
|
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: '' });
|
|
42
71
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, `../deployment/${options.appType}-files`), `./.openshift/${options.projectName}`, templateOptions);
|
|
43
72
|
}
|
|
44
73
|
function addDatabaseFiles(host, options) {
|
|
@@ -47,15 +76,35 @@ function addDatabaseFiles(host, options) {
|
|
|
47
76
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'database-files'), './.openshift/sandbox', { database: options.database, tmpl: '' });
|
|
48
77
|
}
|
|
49
78
|
function addSandboxTarget(host, options) {
|
|
79
|
+
var _a;
|
|
50
80
|
const config = (0, devkit_1.readProjectConfiguration)(host, options.project);
|
|
51
|
-
const { projectName, sandboxProject, database } = options;
|
|
81
|
+
const { projectName, sandboxProject, database, appType, imageRef, registryHost, registryOrg, imageName, } = options;
|
|
52
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
|
+
}
|
|
53
93
|
if (database === 'postgres') {
|
|
54
94
|
commands.push(`oc get secret sandbox-postgres-creds -n ${sandboxProject} 2>/dev/null || ` +
|
|
55
95
|
`oc create secret generic sandbox-postgres-creds ` +
|
|
56
96
|
`--from-literal=POSTGRESQL_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') ` +
|
|
57
97
|
`-n ${sandboxProject}`);
|
|
58
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}"`);
|
|
59
108
|
}
|
|
60
109
|
else if (database === 'mongo') {
|
|
61
110
|
commands.push(`oc get secret sandbox-mongodb-creds -n ${sandboxProject} 2>/dev/null || ` +
|
|
@@ -64,32 +113,98 @@ function addSandboxTarget(host, options) {
|
|
|
64
113
|
`-n ${sandboxProject}`);
|
|
65
114
|
commands.push(`oc apply -f .openshift/sandbox/sandbox-mongodb.yml -n ${sandboxProject}`);
|
|
66
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`);
|
|
67
161
|
commands.push(`oc process -f .openshift/${projectName}/${projectName}.yml -p PROJECT=${sandboxProject} | oc apply -f -`);
|
|
68
|
-
commands.push([
|
|
69
|
-
`REGISTRY=$(oc registry info)`,
|
|
70
|
-
`CONTAINER_CLI=$(command -v podman || command -v docker)`,
|
|
71
|
-
`$CONTAINER_CLI login -u $(oc whoami) -p $(oc whoami -t) $REGISTRY`,
|
|
72
|
-
`$CONTAINER_CLI build -t $REGISTRY/${sandboxProject}/${projectName}:sandbox -f .openshift/${projectName}/Dockerfile .`,
|
|
73
|
-
`$CONTAINER_CLI push $REGISTRY/${sandboxProject}/${projectName}:sandbox`,
|
|
74
|
-
].join(' && '));
|
|
75
162
|
commands.push(`oc rollout restart deployment/${projectName} -n ${sandboxProject}`);
|
|
76
|
-
commands.push(`oc rollout status deployment/${projectName} -n ${sandboxProject} --timeout=
|
|
163
|
+
commands.push(`oc rollout status deployment/${projectName} -n ${sandboxProject} --timeout=180s`);
|
|
77
164
|
config.targets = Object.assign(Object.assign({}, config.targets), { sandbox: {
|
|
78
165
|
executor: 'nx:run-commands',
|
|
79
166
|
options: {
|
|
80
167
|
commands,
|
|
81
|
-
|
|
168
|
+
parallel: false,
|
|
82
169
|
},
|
|
83
170
|
}, 'sandbox-teardown': {
|
|
84
171
|
executor: 'nx:run-commands',
|
|
85
172
|
options: {
|
|
86
173
|
commands: [
|
|
87
|
-
`oc delete all,configmap -l app=${projectName} -n ${sandboxProject} --ignore-not-found`,
|
|
174
|
+
`oc delete all,configmap,is -l app=${projectName} -n ${sandboxProject} --ignore-not-found`,
|
|
175
|
+
`oc delete imagestream ${projectName} -n ${sandboxProject} --ignore-not-found`,
|
|
176
|
+
// Remove the sandbox package (needs delete:packages). Best-effort:
|
|
177
|
+
// sandbox packages are the unlinked ones and can also be pruned org-wide
|
|
178
|
+
// via `select(.repository == null)`.
|
|
179
|
+
`gh api --method DELETE /orgs/${registryOrg}/packages/container/${imageName} 2>/dev/null || true`,
|
|
88
180
|
],
|
|
89
181
|
},
|
|
90
182
|
} });
|
|
91
183
|
(0, devkit_1.updateProjectConfiguration)(host, options.project, config);
|
|
92
184
|
}
|
|
185
|
+
// Register the sandbox deployment's Route with the frontend's public client so
|
|
186
|
+
// browser sign-in works against the deployed URL — not just localhost. The
|
|
187
|
+
// Route host is deterministic by convention (<app>-<namespace>.<ingressDomain>),
|
|
188
|
+
// so this happens once at generate time using the token already obtained for
|
|
189
|
+
// ADSP config — no per-deploy login.
|
|
190
|
+
function registerSandboxRedirectUri(options) {
|
|
191
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
192
|
+
if (options.appType !== 'frontend')
|
|
193
|
+
return;
|
|
194
|
+
const { adsp, projectName, sandboxProject } = options;
|
|
195
|
+
if (!(adsp === null || adsp === void 0 ? void 0 : adsp.accessToken))
|
|
196
|
+
return;
|
|
197
|
+
const ingressDomain = (0, oc_utils_1.getClusterIngressDomain)();
|
|
198
|
+
if (!ingressDomain) {
|
|
199
|
+
console.log('[nx-oc] Could not determine the cluster ingress domain; skipping redirect URI registration. ' +
|
|
200
|
+
'Add the deployment Route to the client manually if browser sign-in fails.');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const routeUrl = `https://${projectName}-${sandboxProject}.${ingressDomain}`;
|
|
204
|
+
const clientId = `urn:ads:${adsp.tenant}:${projectName}`;
|
|
205
|
+
yield (0, adsp_1.addClientRedirectUris)(adsp.accessServiceUrl, adsp.tenantRealm, clientId, [`${routeUrl}/*`], adsp.accessToken);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
93
208
|
function default_1(host, options) {
|
|
94
209
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
95
210
|
const normalizedOptions = yield normalizeOptions(host, options);
|
|
@@ -100,6 +215,7 @@ function default_1(host, options) {
|
|
|
100
215
|
addManifestFiles(host, normalizedOptions);
|
|
101
216
|
addDatabaseFiles(host, normalizedOptions);
|
|
102
217
|
addSandboxTarget(host, normalizedOptions);
|
|
218
|
+
yield registerSandboxRedirectUri(normalizedOptions);
|
|
103
219
|
yield (0, devkit_1.formatFiles)(host);
|
|
104
220
|
});
|
|
105
221
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/sandbox/sandbox.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/sandbox/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -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',
|
|
@@ -84,10 +92,69 @@ describe('Sandbox Generator', () => {
|
|
|
84
92
|
const config = readProjectConfiguration(host, 'test');
|
|
85
93
|
expect(config.targets['sandbox']).toBeTruthy();
|
|
86
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);
|
|
87
97
|
const cmds: string[] = config.targets['sandbox'].options.commands;
|
|
88
98
|
expect(cmds.some((c) => c.includes('oc rollout restart'))).toBeTruthy();
|
|
89
99
|
expect(cmds.some((c) => c.includes('oc rollout status'))).toBeTruthy();
|
|
90
|
-
expect(cmds.some((c) => c.includes('
|
|
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();
|
|
130
|
+
|
|
131
|
+
// The in-cluster BuildConfig flow is gone.
|
|
132
|
+
expect(cmds.some((c) => c.includes('oc start-build'))).toBeFalsy();
|
|
133
|
+
expect(host.exists('.openshift/test/sandbox-build.yml')).toBeFalsy();
|
|
134
|
+
|
|
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();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('persists the resolved registry to nx.json for reuse', async () => {
|
|
147
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
148
|
+
addNodeProject(host);
|
|
149
|
+
|
|
150
|
+
await generator(host, options);
|
|
151
|
+
|
|
152
|
+
const nxJson = readNxJson(host);
|
|
153
|
+
expect(
|
|
154
|
+
(nxJson.generators as Record<string, { registry?: string }>)[
|
|
155
|
+
'@abgov/nx-oc:sandbox'
|
|
156
|
+
].registry
|
|
157
|
+
).toBe('ghcr.io/test-org');
|
|
91
158
|
});
|
|
92
159
|
|
|
93
160
|
it('adds sandbox-teardown nx target', async () => {
|
|
@@ -104,6 +171,12 @@ describe('Sandbox Generator', () => {
|
|
|
104
171
|
expect(cmds.some((c) => c.includes('test-sandbox'))).toBeTruthy();
|
|
105
172
|
expect(cmds.some((c) => c.includes('-l app=test'))).toBeTruthy();
|
|
106
173
|
expect(cmds.some((c) => c.includes('all,configmap'))).toBeTruthy();
|
|
174
|
+
// Teardown also removes the sandbox package (best-effort).
|
|
175
|
+
expect(
|
|
176
|
+
cmds.some((c) =>
|
|
177
|
+
c.includes('packages/container/test-sandbox-test')
|
|
178
|
+
)
|
|
179
|
+
).toBeTruthy();
|
|
107
180
|
});
|
|
108
181
|
|
|
109
182
|
it('generates shared postgres manifest with secret-backed DATABASE_URL', async () => {
|
|
@@ -125,6 +198,12 @@ describe('Sandbox Generator', () => {
|
|
|
125
198
|
const cmds: string[] = config.targets['sandbox'].options.commands;
|
|
126
199
|
expect(cmds.some((c) => c.includes('sandbox-postgres-creds'))).toBeTruthy();
|
|
127
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);
|
|
128
207
|
});
|
|
129
208
|
|
|
130
209
|
it('generates shared mongodb manifest with secret-backed MONGODB_URI', async () => {
|
|
@@ -146,4 +225,62 @@ describe('Sandbox Generator', () => {
|
|
|
146
225
|
const cmds: string[] = config.targets['sandbox'].options.commands;
|
|
147
226
|
expect(cmds.some((c) => c.includes('sandbox-mongodb-creds'))).toBeTruthy();
|
|
148
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();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('registers the deployment Route redirect URI for a frontend client', async () => {
|
|
260
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
261
|
+
addFrontendProject(host);
|
|
262
|
+
utilsMock.addClientRedirectUris.mockClear();
|
|
263
|
+
|
|
264
|
+
await generator(host, options);
|
|
265
|
+
|
|
266
|
+
// Route host follows <app>-<namespace>.<ingressDomain>, registered against
|
|
267
|
+
// the public client urn:ads:<tenant>:<app> with the token from ADSP config.
|
|
268
|
+
expect(utilsMock.addClientRedirectUris).toHaveBeenCalledWith(
|
|
269
|
+
environments.test.accessServiceUrl,
|
|
270
|
+
'test',
|
|
271
|
+
'urn:ads:test:test',
|
|
272
|
+
['https://test-test-sandbox.apps.test.example.com/*'],
|
|
273
|
+
'mock-token'
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('does not register redirect URIs for a node service', async () => {
|
|
278
|
+
const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
279
|
+
addNodeProject(host);
|
|
280
|
+
utilsMock.addClientRedirectUris.mockClear();
|
|
281
|
+
|
|
282
|
+
await generator(host, options);
|
|
283
|
+
|
|
284
|
+
expect(utilsMock.addClientRedirectUris).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
149
286
|
});
|
|
@@ -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,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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"}
|