@credibledata/cred-cli 0.0.102 → 0.0.103
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/README.md +1 -1
- package/coverage/coverage-summary.json +40 -0
- package/coverage/tmp/coverage-2152-1789496974457-0.json +1 -0
- package/coverage/tmp/coverage-2153-1789496954404-0.json +1 -0
- package/dist/client/api.js +4193 -1342
- package/dist/client/api.js.map +1 -1
- package/dist/clientconfiguration.js +57 -4
- package/dist/clientconfiguration.js.map +1 -1
- package/dist/clientconfiguration.spec.js +90 -0
- package/dist/clientconfiguration.spec.js.map +1 -1
- package/dist/commands/add.js +18 -0
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/environment/updateenvironment.js +63 -1
- package/dist/commands/environment/updateenvironment.js.map +1 -1
- package/dist/commands/environment/updateenvironment.spec.js +84 -0
- package/dist/commands/environment/updateenvironment.spec.js.map +1 -1
- package/dist/commands/get.js +52 -1
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/list.js +78 -0
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/materialization/cancelrun.js +62 -0
- package/dist/commands/materialization/cancelrun.js.map +1 -0
- package/dist/commands/materialization/getindex.js +52 -0
- package/dist/commands/materialization/getindex.js.map +1 -0
- package/dist/commands/materialization/getmaterialization.js +54 -0
- package/dist/commands/materialization/getmaterialization.js.map +1 -0
- package/dist/commands/materialization/getpreaggregate.js +55 -0
- package/dist/commands/materialization/getpreaggregate.js.map +1 -0
- package/dist/commands/materialization/getrun.js +54 -0
- package/dist/commands/materialization/getrun.js.map +1 -0
- package/dist/commands/materialization/listindexes.js +58 -0
- package/dist/commands/materialization/listindexes.js.map +1 -0
- package/dist/commands/materialization/listmaterializations.js +52 -0
- package/dist/commands/materialization/listmaterializations.js.map +1 -0
- package/dist/commands/materialization/listpreaggregates.js +61 -0
- package/dist/commands/materialization/listpreaggregates.js.map +1 -0
- package/dist/commands/materialization/listruns.js +60 -0
- package/dist/commands/materialization/listruns.js.map +1 -0
- package/dist/commands/materialization/rerun.js +111 -0
- package/dist/commands/materialization/rerun.js.map +1 -0
- package/dist/commands/misc/login.js +14 -0
- package/dist/commands/misc/login.js.map +1 -1
- package/dist/commands/misc/login.spec.js +92 -0
- package/dist/commands/misc/login.spec.js.map +1 -1
- package/dist/commands/package/listpackages.js +7 -0
- package/dist/commands/package/listpackages.js.map +1 -1
- package/dist/commands/package/listpackages.spec.js +12 -0
- package/dist/commands/package/listpackages.spec.js.map +1 -1
- package/dist/commands/package/publish.js +34 -11
- package/dist/commands/package/publish.js.map +1 -1
- package/dist/commands/package/publish.spec.js +26 -28
- package/dist/commands/package/publish.spec.js.map +1 -1
- package/dist/commands/permission/createpermission.js +123 -0
- package/dist/commands/permission/createpermission.js.map +1 -0
- package/dist/commands/permission/createpermission.spec.js +360 -0
- package/dist/commands/permission/createpermission.spec.js.map +1 -0
- package/dist/commands/permission/deletepermission.js +86 -0
- package/dist/commands/permission/deletepermission.js.map +1 -0
- package/dist/commands/permission/getpermission.js +85 -0
- package/dist/commands/permission/getpermission.js.map +1 -0
- package/dist/commands/permission/listpermissions.js +75 -0
- package/dist/commands/permission/listpermissions.js.map +1 -0
- package/dist/commands/permission/scope.js +140 -0
- package/dist/commands/permission/scope.js.map +1 -0
- package/dist/commands/rm.js +11 -0
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/update.js +18 -12
- package/dist/commands/update.js.map +1 -1
- package/dist/common/UserGroupId.js +1 -1
- package/dist/common/UserGroupId.js.map +1 -1
- package/dist/common/UserGroupId.spec.js +2 -4
- package/dist/common/UserGroupId.spec.js.map +1 -1
- package/dist/constants.js +48 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +61 -6
- package/dist/index.js.map +1 -1
- package/dist/logger.js +5 -1
- package/dist/logger.js.map +1 -1
- package/openapitools.json +4 -1
- package/package.json +5 -3
- package/src/client/api.ts +6433 -1533
- package/src/clientconfiguration.spec.ts +126 -0
- package/src/clientconfiguration.ts +64 -4
- package/src/commands/add.ts +43 -0
- package/src/commands/environment/updateenvironment.spec.ts +167 -0
- package/src/commands/environment/updateenvironment.ts +85 -1
- package/src/commands/get.ts +110 -1
- package/src/commands/list.ts +171 -0
- package/src/commands/materialization/cancelrun.ts +70 -0
- package/src/commands/materialization/getindex.ts +55 -0
- package/src/commands/materialization/getmaterialization.ts +57 -0
- package/src/commands/materialization/getpreaggregate.ts +58 -0
- package/src/commands/materialization/getrun.ts +55 -0
- package/src/commands/materialization/listindexes.ts +69 -0
- package/src/commands/materialization/listmaterializations.ts +53 -0
- package/src/commands/materialization/listpreaggregates.ts +74 -0
- package/src/commands/materialization/listruns.ts +77 -0
- package/src/commands/materialization/rerun.ts +123 -0
- package/src/commands/misc/login.spec.ts +118 -2
- package/src/commands/misc/login.ts +14 -0
- package/src/commands/package/listpackages.spec.ts +16 -0
- package/src/commands/package/listpackages.ts +7 -0
- package/src/commands/package/publish.spec.ts +45 -30
- package/src/commands/package/publish.ts +31 -10
- package/src/commands/permission/createpermission.spec.ts +507 -0
- package/src/commands/permission/createpermission.ts +181 -0
- package/src/commands/permission/deletepermission.ts +117 -0
- package/src/commands/permission/getpermission.ts +115 -0
- package/src/commands/permission/listpermissions.ts +101 -0
- package/src/commands/permission/scope.ts +180 -0
- package/src/commands/rm.ts +29 -0
- package/src/commands/update.ts +50 -27
- package/src/common/UserGroupId.spec.ts +2 -4
- package/src/common/UserGroupId.ts +1 -1
- package/src/constants.ts +50 -1
- package/src/index.ts +94 -11
- package/src/logger.ts +5 -1
- package/bun.lock +0 -934
|
@@ -313,6 +313,74 @@ describe("ClientConfiguration", () => {
|
|
|
313
313
|
expect(writeFileSyncStub.called).to.be.false;
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
+
describe("recoverIfCorrupt", () => {
|
|
317
|
+
it("returns null and writes nothing when the config is already valid", () => {
|
|
318
|
+
readFileSyncStub.returns(
|
|
319
|
+
JSON.stringify({
|
|
320
|
+
configPerEnvironment: {},
|
|
321
|
+
activeEnvironment: "staging",
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(clientConfig.recoverIfCorrupt()).to.be.null;
|
|
326
|
+
expect(renameSyncStub.called).to.be.false;
|
|
327
|
+
// Valid config preserved — active environment untouched.
|
|
328
|
+
expect(clientConfig.getEnvironment()).to.equal("staging");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("backs up an unparseable file and resets to defaults", () => {
|
|
332
|
+
readFileSyncStub.returns("{ not json");
|
|
333
|
+
|
|
334
|
+
const backupPath = clientConfig.recoverIfCorrupt();
|
|
335
|
+
|
|
336
|
+
expect(backupPath).to.match(
|
|
337
|
+
new RegExp(`${customConfigPath}\\.corrupt-[0-9a-f]{16}$`),
|
|
338
|
+
);
|
|
339
|
+
// Renamed aside, never deleted, so the bad file is recoverable.
|
|
340
|
+
expect(renameSyncStub.calledWith(customConfigPath, backupPath!)).to.be
|
|
341
|
+
.true;
|
|
342
|
+
expect(lastWrittenConfig()).to.deep.equal({
|
|
343
|
+
configPerEnvironment: fullPerEnv(),
|
|
344
|
+
activeEnvironment: "production",
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("backs up a structurally-invalid file and resets to defaults", () => {
|
|
349
|
+
readFileSyncStub.returns(JSON.stringify({ configPerEnvironment: {} }));
|
|
350
|
+
|
|
351
|
+
const backupPath = clientConfig.recoverIfCorrupt();
|
|
352
|
+
|
|
353
|
+
expect(backupPath).to.be.a("string");
|
|
354
|
+
expect(renameSyncStub.calledWith(customConfigPath, backupPath!)).to.be
|
|
355
|
+
.true;
|
|
356
|
+
expect(lastWrittenConfig().activeEnvironment).to.equal("production");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("rethrows fs errors (e.g. EACCES) instead of masking them as a reset", () => {
|
|
360
|
+
// A permissions problem must surface — resetting would just fail to
|
|
361
|
+
// write anyway, and silently "recovering" would hide the real cause.
|
|
362
|
+
const eacces = Object.assign(new Error("EACCES"), { code: "EACCES" });
|
|
363
|
+
readFileSyncStub.throws(eacces);
|
|
364
|
+
|
|
365
|
+
expect(() => clientConfig.recoverIfCorrupt()).to.throw(/EACCES/);
|
|
366
|
+
expect(renameSyncStub.called).to.be.false;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("creates a fresh default when the file is absent (ENOENT)", () => {
|
|
370
|
+
readFileSyncStub.throws(enoent());
|
|
371
|
+
|
|
372
|
+
// First-time setup is not corruption — nothing to back up. (The atomic
|
|
373
|
+
// write of the new default does call renameSync, but never to a
|
|
374
|
+
// `.corrupt-` backup path.)
|
|
375
|
+
expect(clientConfig.recoverIfCorrupt()).to.be.null;
|
|
376
|
+
expect(
|
|
377
|
+
renameSyncStub
|
|
378
|
+
.getCalls()
|
|
379
|
+
.some((c) => String(c.args[1]).includes(".corrupt-")),
|
|
380
|
+
).to.be.false;
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
316
384
|
describe("isTokenValid", () => {
|
|
317
385
|
it("should return invalid when no token exists", () => {
|
|
318
386
|
const mockConfig = {
|
|
@@ -468,6 +536,64 @@ describe("ClientConfiguration", () => {
|
|
|
468
536
|
.true;
|
|
469
537
|
});
|
|
470
538
|
|
|
539
|
+
it("should name the targeted cell when the token is valid", () => {
|
|
540
|
+
const payload = { exp: Math.floor(Date.now() / 1000) + 3600 };
|
|
541
|
+
const fakeJwt =
|
|
542
|
+
Buffer.from(JSON.stringify({ alg: "HS256" })).toString(
|
|
543
|
+
"base64url",
|
|
544
|
+
) +
|
|
545
|
+
"." +
|
|
546
|
+
Buffer.from(JSON.stringify(payload)).toString("base64url") +
|
|
547
|
+
".signature";
|
|
548
|
+
|
|
549
|
+
const mockConfig = {
|
|
550
|
+
configPerEnvironment: {
|
|
551
|
+
production: {},
|
|
552
|
+
staging: {},
|
|
553
|
+
localhost: {},
|
|
554
|
+
dev: {
|
|
555
|
+
accessToken: fakeJwt,
|
|
556
|
+
isServiceAccountToken: false,
|
|
557
|
+
organization: "demo",
|
|
558
|
+
project: "demo",
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
activeEnvironment: "dev",
|
|
562
|
+
};
|
|
563
|
+
readFileSyncStub.returns(JSON.stringify(mockConfig));
|
|
564
|
+
|
|
565
|
+
clientConfig.printStatus();
|
|
566
|
+
|
|
567
|
+
expect(loggerStub.info.calledWith(sinon.match("Cell:"))).to.be.true;
|
|
568
|
+
expect(loggerStub.info.calledWith(sinon.match("dev"))).to.be.true;
|
|
569
|
+
expect(
|
|
570
|
+
loggerStub.info.calledWith(
|
|
571
|
+
sinon.match("https://dev-admin.credibledata.com/api/v0"),
|
|
572
|
+
),
|
|
573
|
+
).to.be.true;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("should name the targeted cell even when the token is invalid", () => {
|
|
577
|
+
const mockConfig = {
|
|
578
|
+
configPerEnvironment: {
|
|
579
|
+
production: {},
|
|
580
|
+
staging: {},
|
|
581
|
+
localhost: {},
|
|
582
|
+
},
|
|
583
|
+
activeEnvironment: "localhost",
|
|
584
|
+
};
|
|
585
|
+
readFileSyncStub.returns(JSON.stringify(mockConfig));
|
|
586
|
+
|
|
587
|
+
clientConfig.printStatus();
|
|
588
|
+
|
|
589
|
+
expect(loggerStub.info.calledWith(sinon.match("Cell:"))).to.be.true;
|
|
590
|
+
expect(
|
|
591
|
+
loggerStub.info.calledWith(
|
|
592
|
+
sinon.match("http://admin.ms2.localhost/api/v0"),
|
|
593
|
+
),
|
|
594
|
+
).to.be.true;
|
|
595
|
+
});
|
|
596
|
+
|
|
471
597
|
it("should print error when token is invalid", () => {
|
|
472
598
|
const mockConfig = {
|
|
473
599
|
configPerEnvironment: {
|
|
@@ -3,7 +3,12 @@ import fs from "fs";
|
|
|
3
3
|
import { jwtDecode } from "jwt-decode";
|
|
4
4
|
import kleur from "kleur";
|
|
5
5
|
import os from "os";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ALL_ENVIRONMENTS,
|
|
8
|
+
ASCII_TITLE,
|
|
9
|
+
controlPlanesByEnv,
|
|
10
|
+
Environment,
|
|
11
|
+
} from "./constants";
|
|
7
12
|
import { logger } from "./logger";
|
|
8
13
|
|
|
9
14
|
interface ConfigFile {
|
|
@@ -34,6 +39,15 @@ interface JwtPayload {
|
|
|
34
39
|
sub?: string;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when `.cred` exists but cannot be used: either it does not parse as
|
|
44
|
+
* JSON or it parses but lacks the required top-level shape. Distinct from fs
|
|
45
|
+
* errors (ENOENT, EACCES) so callers that want to recover — `cred login` —
|
|
46
|
+
* can target *only* a corrupt-content failure and leave permission/IO errors
|
|
47
|
+
* to surface loudly.
|
|
48
|
+
*/
|
|
49
|
+
export class InvalidConfigError extends Error {}
|
|
50
|
+
|
|
37
51
|
export class ClientConfiguration {
|
|
38
52
|
private configPath = `${os.homedir()}/.cred`;
|
|
39
53
|
private config: ConfigFile;
|
|
@@ -65,13 +79,13 @@ export class ClientConfiguration {
|
|
|
65
79
|
// can leave `.cred` unparseable. Fail loud rather than silently
|
|
66
80
|
// overwriting — a silent reset erases the access token for every
|
|
67
81
|
// subsequent caller on the same machine.
|
|
68
|
-
throw new
|
|
82
|
+
throw new InvalidConfigError(
|
|
69
83
|
`Failed to parse ${this.configPath}: ${error}. The file may be corrupt; re-run \`cred login\` to reset.`,
|
|
70
84
|
);
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
if (!this.config.activeEnvironment || !this.config.configPerEnvironment) {
|
|
74
|
-
throw new
|
|
88
|
+
throw new InvalidConfigError(
|
|
75
89
|
`Invalid config structure in ${this.configPath}; re-run \`cred login\` to reset.`,
|
|
76
90
|
);
|
|
77
91
|
}
|
|
@@ -85,6 +99,46 @@ export class ClientConfiguration {
|
|
|
85
99
|
return this.config;
|
|
86
100
|
}
|
|
87
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Ensure `.cred` is usable, recovering from a corrupt file if needed.
|
|
104
|
+
*
|
|
105
|
+
* `loadConfig` deliberately fails loud on a corrupt/invalid file so a read
|
|
106
|
+
* path never silently erases tokens. But `cred login` exists precisely to
|
|
107
|
+
* (re)establish config, so it must be able to get past a bad file — that is
|
|
108
|
+
* what the "re-run `cred login` to reset" message promises. We make the
|
|
109
|
+
* message true here: the bad file is renamed aside (never deleted, so a
|
|
110
|
+
* support engineer can still inspect it) and a fresh default is written.
|
|
111
|
+
*
|
|
112
|
+
* Only an {@link InvalidConfigError} triggers recovery. fs errors such as
|
|
113
|
+
* EACCES are real environment problems and are rethrown — resetting would
|
|
114
|
+
* just fail to write anyway, and we must not mask a permissions issue as a
|
|
115
|
+
* successful reset.
|
|
116
|
+
*
|
|
117
|
+
* @returns the path the corrupt file was backed up to, or null if the
|
|
118
|
+
* existing config was already valid.
|
|
119
|
+
*/
|
|
120
|
+
public recoverIfCorrupt(): string | null {
|
|
121
|
+
try {
|
|
122
|
+
this.loadConfig();
|
|
123
|
+
return null;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!(error instanceof InvalidConfigError)) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
const backupPath = `${this.configPath}.corrupt-${crypto
|
|
129
|
+
.randomBytes(8)
|
|
130
|
+
.toString("hex")}`;
|
|
131
|
+
fs.renameSync(this.configPath, backupPath);
|
|
132
|
+
logger.warn(
|
|
133
|
+
`Existing config at ${this.configPath} was invalid and could not be read. ` +
|
|
134
|
+
`Backed it up to ${backupPath} and reset to defaults.`,
|
|
135
|
+
);
|
|
136
|
+
this.config = this.defaultConfig();
|
|
137
|
+
this.writeConfig();
|
|
138
|
+
return backupPath;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
88
142
|
private defaultConfig(): ConfigFile {
|
|
89
143
|
const configPerEnvironment: ConfigFile["configPerEnvironment"] = {};
|
|
90
144
|
for (const env of ALL_ENVIRONMENTS) {
|
|
@@ -286,12 +340,18 @@ export class ClientConfiguration {
|
|
|
286
340
|
public printStatus() {
|
|
287
341
|
this.loadConfig();
|
|
288
342
|
const tokenStatus = this.isTokenValid();
|
|
343
|
+
const cell = this.getEnvironment();
|
|
344
|
+
|
|
345
|
+
// The cell prints on both branches, and before anything else: an org and an
|
|
346
|
+
// environment name alone do not identify a cell (two cells can both hold a
|
|
347
|
+
// `demo`), and a stale token is most often a token for the wrong one.
|
|
348
|
+
console.log(kleur.green(ASCII_TITLE));
|
|
349
|
+
logger.info(`Cell: ${kleur.green(cell)} (${controlPlanesByEnv[cell]})`);
|
|
289
350
|
|
|
290
351
|
if (tokenStatus.valid) {
|
|
291
352
|
const organization = this.getOrganization();
|
|
292
353
|
const project = this.getProject();
|
|
293
354
|
|
|
294
|
-
console.log(kleur.green(ASCII_TITLE));
|
|
295
355
|
logger.info(`Organization: ${kleur.green(organization || "Not set")}`);
|
|
296
356
|
logger.info(`Environment: ${kleur.green(project || "Not set")}`);
|
|
297
357
|
} else {
|
package/src/commands/add.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createGroup } from "./group/creategroup";
|
|
|
7
7
|
import { createGroupAccessToken } from "./groupaccesstoken/createToken";
|
|
8
8
|
import { createEnvironment } from "./environment/createenvironment";
|
|
9
9
|
import { warnProjectDeprecated } from "./legacy";
|
|
10
|
+
import { createPermission } from "./permission/createpermission";
|
|
10
11
|
|
|
11
12
|
export const registerAddCommands = (cli: Command, environment: Environment) => {
|
|
12
13
|
const add = cli
|
|
@@ -125,6 +126,48 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
|
|
|
125
126
|
);
|
|
126
127
|
});
|
|
127
128
|
|
|
129
|
+
add.command("permission")
|
|
130
|
+
.description(
|
|
131
|
+
"Grant a user or group a permission on the organization, an environment, or a package.",
|
|
132
|
+
)
|
|
133
|
+
.argument(
|
|
134
|
+
"<userGroupId>",
|
|
135
|
+
"Who to grant it to [user:{email}|group:{groupName}|user:*]",
|
|
136
|
+
)
|
|
137
|
+
.argument(
|
|
138
|
+
"<role>",
|
|
139
|
+
"The role to grant (organization: admin|modeler|member; environment: admin|modeler|viewer; package: viewer)",
|
|
140
|
+
)
|
|
141
|
+
.option(
|
|
142
|
+
"--environment <environmentName>",
|
|
143
|
+
"Grant on this environment instead of the organization.",
|
|
144
|
+
)
|
|
145
|
+
.option(
|
|
146
|
+
"--package <packageName>",
|
|
147
|
+
"Grant on this package. Uses --environment, or the current environment.",
|
|
148
|
+
)
|
|
149
|
+
.option(
|
|
150
|
+
"--notifyPeople",
|
|
151
|
+
"Email the person about the new access. Applies to an environment or a package; an organization grant always notifies a user subject.",
|
|
152
|
+
false,
|
|
153
|
+
)
|
|
154
|
+
.option("--debug")
|
|
155
|
+
.option("-y, --yes", "Skip confirmation.")
|
|
156
|
+
.action((userGroupId, role, options) => {
|
|
157
|
+
createPermission(
|
|
158
|
+
controlPlanesByEnv[environment],
|
|
159
|
+
userGroupId,
|
|
160
|
+
role,
|
|
161
|
+
options.debug || false,
|
|
162
|
+
{
|
|
163
|
+
environment: options.environment,
|
|
164
|
+
package: options.package,
|
|
165
|
+
notifyPeople: options.notifyPeople === true,
|
|
166
|
+
yes: options.yes === true,
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
128
171
|
add.command("group-access-token")
|
|
129
172
|
.description("Create an access token for a group.")
|
|
130
173
|
.argument("<groupName>", "The name of the group")
|
|
@@ -158,6 +158,173 @@ describe("update-environment", () => {
|
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
describe("git-backed modeling", () => {
|
|
162
|
+
it("sends the enrolment flag on its own", async () => {
|
|
163
|
+
updateEnvironmentStub.resolves({ status: 200 });
|
|
164
|
+
|
|
165
|
+
await updateEnvironment(
|
|
166
|
+
"http://test-server",
|
|
167
|
+
"test-environment",
|
|
168
|
+
{ gitModeling: "true" },
|
|
169
|
+
false,
|
|
170
|
+
true,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(updateEnvironmentStub.calledOnce).to.be.true;
|
|
174
|
+
expect(updateEnvironmentStub.firstCall.args[2]).to.deep.equal({
|
|
175
|
+
gitModelingEnabled: true,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("rejects a --git-modeling value that is neither true nor false", async () => {
|
|
180
|
+
await updateEnvironment(
|
|
181
|
+
"http://test-server",
|
|
182
|
+
"test-environment",
|
|
183
|
+
{ gitModeling: "yes" },
|
|
184
|
+
false,
|
|
185
|
+
true,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(updateEnvironmentStub.called).to.be.false;
|
|
189
|
+
expect(
|
|
190
|
+
loggerStub.error.calledWith(
|
|
191
|
+
sinon.match("--git-modeling must be true or false"),
|
|
192
|
+
),
|
|
193
|
+
).to.be.true;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("sends all three coordinates together", async () => {
|
|
197
|
+
updateEnvironmentStub.resolves({ status: 200 });
|
|
198
|
+
|
|
199
|
+
await updateEnvironment(
|
|
200
|
+
"http://test-server",
|
|
201
|
+
"test-environment",
|
|
202
|
+
{
|
|
203
|
+
gitRepositoryOwner: "credible-data-repos",
|
|
204
|
+
gitRepositoryName: "models",
|
|
205
|
+
gitBaseBranch: "main",
|
|
206
|
+
},
|
|
207
|
+
false,
|
|
208
|
+
true,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(updateEnvironmentStub.firstCall.args[2]).to.deep.equal({
|
|
212
|
+
gitRepositoryOwner: "credible-data-repos",
|
|
213
|
+
gitRepositoryName: "models",
|
|
214
|
+
gitBaseBranch: "main",
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("refuses a partial coordinate locally rather than sending a 400", async () => {
|
|
219
|
+
// The API rejects this as GIT_REPOSITORY_INCOMPLETE. Refusing here means a
|
|
220
|
+
// script gets the rule from the tool it is driving.
|
|
221
|
+
await updateEnvironment(
|
|
222
|
+
"http://test-server",
|
|
223
|
+
"test-environment",
|
|
224
|
+
{ gitRepositoryOwner: "credible-data-repos" },
|
|
225
|
+
false,
|
|
226
|
+
true,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(updateEnvironmentStub.called).to.be.false;
|
|
230
|
+
expect(loggerStub.error.calledWith(sinon.match("change together")))
|
|
231
|
+
.to.be.true;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("refuses a mix of set and cleared coordinates", async () => {
|
|
235
|
+
await updateEnvironment(
|
|
236
|
+
"http://test-server",
|
|
237
|
+
"test-environment",
|
|
238
|
+
{
|
|
239
|
+
gitRepositoryOwner: "credible-data-repos",
|
|
240
|
+
gitRepositoryName: "",
|
|
241
|
+
gitBaseBranch: "main",
|
|
242
|
+
},
|
|
243
|
+
false,
|
|
244
|
+
true,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(updateEnvironmentStub.called).to.be.false;
|
|
248
|
+
expect(
|
|
249
|
+
loggerStub.error.calledWith(sinon.match("unset the repository")),
|
|
250
|
+
).to.be.true;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("sends nulls when all three are emptied, which is how a record is unset", async () => {
|
|
254
|
+
updateEnvironmentStub.resolves({ status: 200 });
|
|
255
|
+
|
|
256
|
+
await updateEnvironment(
|
|
257
|
+
"http://test-server",
|
|
258
|
+
"test-environment",
|
|
259
|
+
{
|
|
260
|
+
gitRepositoryOwner: "",
|
|
261
|
+
gitRepositoryName: "",
|
|
262
|
+
gitBaseBranch: " ",
|
|
263
|
+
},
|
|
264
|
+
false,
|
|
265
|
+
true,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(updateEnvironmentStub.firstCall.args[2]).to.deep.equal({
|
|
269
|
+
gitRepositoryOwner: null,
|
|
270
|
+
gitRepositoryName: null,
|
|
271
|
+
gitBaseBranch: null,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("carries the enrolment and the coordinates in one request", async () => {
|
|
276
|
+
updateEnvironmentStub.resolves({ status: 200 });
|
|
277
|
+
|
|
278
|
+
await updateEnvironment(
|
|
279
|
+
"http://test-server",
|
|
280
|
+
"test-environment",
|
|
281
|
+
{
|
|
282
|
+
gitModeling: "true",
|
|
283
|
+
gitRepositoryOwner: "credible-data-repos",
|
|
284
|
+
gitRepositoryName: "models",
|
|
285
|
+
gitBaseBranch: "main",
|
|
286
|
+
},
|
|
287
|
+
false,
|
|
288
|
+
true,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(updateEnvironmentStub.firstCall.args[2]).to.deep.equal({
|
|
292
|
+
gitModelingEnabled: true,
|
|
293
|
+
gitRepositoryOwner: "credible-data-repos",
|
|
294
|
+
gitRepositoryName: "models",
|
|
295
|
+
gitBaseBranch: "main",
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("reports a refusal from the server rather than claiming success", async () => {
|
|
300
|
+
updateEnvironmentStub.rejects(
|
|
301
|
+
Object.assign(new Error("Request failed with status code 400"), {
|
|
302
|
+
response: {
|
|
303
|
+
status: 400,
|
|
304
|
+
data: { code: "GIT_REPOSITORY_NOT_REGISTERED" },
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await updateEnvironment(
|
|
310
|
+
"http://test-server",
|
|
311
|
+
"test-environment",
|
|
312
|
+
{ gitModeling: "true" },
|
|
313
|
+
false,
|
|
314
|
+
true,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(
|
|
318
|
+
loggerStub.error.calledWith(
|
|
319
|
+
sinon.match("Failed to update environment test-environment"),
|
|
320
|
+
),
|
|
321
|
+
).to.be.true;
|
|
322
|
+
expect(
|
|
323
|
+
loggerStub.info.calledWith(sinon.match("Successfully updated")),
|
|
324
|
+
).to.be.false;
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
161
328
|
describe("replication count validation", () => {
|
|
162
329
|
it("should accept valid replication counts", async () => {
|
|
163
330
|
const mockResponse = { status: 200 };
|
|
@@ -8,8 +8,60 @@ const environmentsApi = new EnvironmentsApi(new Configuration());
|
|
|
8
8
|
|
|
9
9
|
type UpdateEnvironmentProperties = {
|
|
10
10
|
replicationCount?: number;
|
|
11
|
+
/** Raw `--git-modeling` value; parsed here so a typo is refused rather than sent. */
|
|
12
|
+
gitModeling?: string;
|
|
13
|
+
gitRepositoryOwner?: string;
|
|
14
|
+
gitRepositoryName?: string;
|
|
15
|
+
gitBaseBranch?: string;
|
|
11
16
|
};
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The three repository coordinates change as a unit, and the API rejects any other
|
|
20
|
+
* shape (`GIT_REPOSITORY_INCOMPLETE`, `GIT_REPOSITORY_INCOHERENT`). Refused here too, so
|
|
21
|
+
* a scripted caller gets the rule from the tool it is driving rather than from a 400.
|
|
22
|
+
*
|
|
23
|
+
* Returns the body fragment, or a message to print instead of sending anything.
|
|
24
|
+
*/
|
|
25
|
+
function gitRepositoryRecord(
|
|
26
|
+
properties: UpdateEnvironmentProperties,
|
|
27
|
+
): { changes: Partial<Environment> } | { error: string } {
|
|
28
|
+
const mentioned = [
|
|
29
|
+
properties.gitRepositoryOwner,
|
|
30
|
+
properties.gitRepositoryName,
|
|
31
|
+
properties.gitBaseBranch,
|
|
32
|
+
].filter((value) => value !== undefined) as string[];
|
|
33
|
+
|
|
34
|
+
if (mentioned.length === 0) {
|
|
35
|
+
return { changes: {} };
|
|
36
|
+
}
|
|
37
|
+
if (mentioned.length !== 3) {
|
|
38
|
+
return {
|
|
39
|
+
error:
|
|
40
|
+
"--git-repository-owner, --git-repository-name and --git-base-branch change " +
|
|
41
|
+
"together. Pass all three, or none.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const trimmed = mentioned.map((value) => value.trim());
|
|
46
|
+
const cleared = trimmed.filter((value) => value === "").length;
|
|
47
|
+
if (cleared !== 0 && cleared !== 3) {
|
|
48
|
+
return {
|
|
49
|
+
error:
|
|
50
|
+
"Set all three of --git-repository-owner, --git-repository-name and " +
|
|
51
|
+
'--git-base-branch, or pass "" for all three to unset the repository.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const [owner, name, baseBranch] = trimmed;
|
|
56
|
+
return {
|
|
57
|
+
changes: {
|
|
58
|
+
gitRepositoryOwner: owner || null,
|
|
59
|
+
gitRepositoryName: name || null,
|
|
60
|
+
gitBaseBranch: baseBranch || null,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
13
65
|
export async function updateEnvironment(
|
|
14
66
|
controlPlane: string,
|
|
15
67
|
environmentName: string,
|
|
@@ -38,6 +90,21 @@ export async function updateEnvironment(
|
|
|
38
90
|
return;
|
|
39
91
|
}
|
|
40
92
|
|
|
93
|
+
if (
|
|
94
|
+
properties.gitModeling !== undefined &&
|
|
95
|
+
properties.gitModeling !== "true" &&
|
|
96
|
+
properties.gitModeling !== "false"
|
|
97
|
+
) {
|
|
98
|
+
logger.error("--git-modeling must be true or false");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const repositoryRecord = gitRepositoryRecord(properties);
|
|
103
|
+
if ("error" in repositoryRecord) {
|
|
104
|
+
logger.error(repositoryRecord.error);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
41
108
|
try {
|
|
42
109
|
if (!skipConfirmation) {
|
|
43
110
|
const confirmation = await getConfirmation(
|
|
@@ -49,12 +116,17 @@ export async function updateEnvironment(
|
|
|
49
116
|
}
|
|
50
117
|
}
|
|
51
118
|
|
|
52
|
-
const environmentUpdate: Environment = {};
|
|
119
|
+
const environmentUpdate: Environment = { ...repositoryRecord.changes };
|
|
53
120
|
|
|
54
121
|
if (properties.replicationCount !== undefined) {
|
|
55
122
|
environmentUpdate.replicationCount = properties.replicationCount;
|
|
56
123
|
}
|
|
57
124
|
|
|
125
|
+
if (properties.gitModeling !== undefined) {
|
|
126
|
+
environmentUpdate.gitModelingEnabled =
|
|
127
|
+
properties.gitModeling === "true";
|
|
128
|
+
}
|
|
129
|
+
|
|
58
130
|
const response = await environmentsApi.updateEnvironment(
|
|
59
131
|
organizationName,
|
|
60
132
|
environmentName,
|
|
@@ -72,6 +144,18 @@ export async function updateEnvironment(
|
|
|
72
144
|
`Replication count set to ${properties.replicationCount}`,
|
|
73
145
|
);
|
|
74
146
|
}
|
|
147
|
+
if (properties.gitModeling !== undefined) {
|
|
148
|
+
logger.info(
|
|
149
|
+
`Git-backed modeling ${properties.gitModeling === "true" ? "enabled" : "disabled"}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (environmentUpdate.gitRepositoryOwner !== undefined) {
|
|
153
|
+
logger.info(
|
|
154
|
+
environmentUpdate.gitRepositoryOwner
|
|
155
|
+
? `Repository set to ${environmentUpdate.gitRepositoryOwner}/${environmentUpdate.gitRepositoryName} (${environmentUpdate.gitBaseBranch})`
|
|
156
|
+
: "Repository unset",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
75
159
|
} else {
|
|
76
160
|
logger.error(
|
|
77
161
|
`Failed to update environment. Status: ${response.status}`,
|