@contrail/cli 3.5.4-alpha.f60c4df → 3.5.5-alpha.5c010ac

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.
Files changed (54) hide show
  1. package/README.md +112 -23
  2. package/dist/api/feature-flags.d.ts +3 -0
  3. package/dist/api/feature-flags.js +29 -0
  4. package/dist/api/org-config.d.ts +3 -0
  5. package/dist/api/org-config.js +28 -0
  6. package/dist/api/users.d.ts +1 -0
  7. package/dist/api/users.js +10 -1
  8. package/dist/commands/feature-flags/export.d.ts +19 -0
  9. package/dist/commands/feature-flags/export.js +66 -0
  10. package/dist/commands/feature-flags/export.spec.d.ts +1 -0
  11. package/dist/commands/feature-flags/export.spec.js +109 -0
  12. package/dist/commands/feature-flags/import.d.ts +28 -0
  13. package/dist/commands/feature-flags/import.js +151 -0
  14. package/dist/commands/feature-flags/import.spec.d.ts +1 -0
  15. package/dist/commands/feature-flags/import.spec.js +125 -0
  16. package/dist/commands/groups/export.js +1 -13
  17. package/dist/commands/groups/import.js +2 -15
  18. package/dist/commands/instance/import.d.ts +14 -0
  19. package/dist/commands/instance/import.js +27 -5
  20. package/dist/commands/instance/import.spec.d.ts +1 -0
  21. package/dist/commands/instance/import.spec.js +57 -0
  22. package/dist/commands/org-config/export.d.ts +13 -0
  23. package/dist/commands/org-config/export.js +74 -0
  24. package/dist/commands/org-config/import.d.ts +12 -0
  25. package/dist/commands/org-config/import.js +63 -0
  26. package/dist/commands/types/getAll.js +2 -0
  27. package/dist/commands/types/loadAll.d.ts +13 -1
  28. package/dist/commands/types/loadAll.js +41 -4
  29. package/dist/commands/types/loadAll.spec.d.ts +1 -0
  30. package/dist/commands/types/loadAll.spec.js +77 -0
  31. package/dist/commands/types/mergeOptionSets.js +2 -9
  32. package/dist/commands/types/mergeTypeProperties.js +2 -6
  33. package/dist/commands/types/parseOptionSetsLocalFiles.js +3 -7
  34. package/dist/commands/types/uploadTypeOverrides.d.ts +10 -0
  35. package/dist/commands/types/uploadTypeOverrides.js +19 -8
  36. package/dist/commands/types/uploadTypeOverrides.spec.d.ts +1 -0
  37. package/dist/commands/types/uploadTypeOverrides.spec.js +60 -0
  38. package/dist/commands/users/export.js +1 -13
  39. package/dist/commands/users/import.js +2 -15
  40. package/dist/commands/workflow-templates/export.d.ts +2 -3
  41. package/dist/commands/workflow-templates/export.js +23 -122
  42. package/dist/commands/workflow-templates/load.d.ts +0 -2
  43. package/dist/commands/workflow-templates/load.js +6 -63
  44. package/dist/helpers/error-helpers.d.ts +9 -0
  45. package/dist/helpers/error-helpers.js +35 -0
  46. package/dist/helpers/file-helpers.d.ts +2 -0
  47. package/dist/helpers/file-helpers.js +25 -1
  48. package/dist/helpers/projects/helpers.js +2 -6
  49. package/dist/helpers/types/type-parser.js +9 -15
  50. package/dist/helpers/workflow-templates/helper.d.ts +1 -2
  51. package/dist/helpers/workflow-templates/helper.js +6 -36
  52. package/dist/interfaces.d.ts +1 -1
  53. package/oclif.manifest.json +236 -30
  54. package/package.json +7 -1
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadFeatureFlags = exports.assertCanManageFeatureFlags = exports.REQUIRED_PLATFORM_ROLES = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const path = tslib_1.__importStar(require("path"));
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const fs_1 = tslib_1.__importDefault(require("fs"));
8
+ const login_1 = require("../../auth/login");
9
+ const core_1 = require("@oclif/core");
10
+ const file_helpers_1 = require("../../helpers/file-helpers");
11
+ const error_helpers_1 = require("../../helpers/error-helpers");
12
+ const feature_flags_1 = require("../../api/feature-flags");
13
+ const users_1 = require("../../api/users");
14
+ const export_1 = require("./export");
15
+ const basecommand_1 = require("../../basecommand");
16
+ const logger_1 = tslib_1.__importDefault(require("../../logger"));
17
+ exports.REQUIRED_PLATFORM_ROLES = ['PLATFORM_ADMIN', 'PLATFORM_MANAGER'];
18
+ class ImportFeatureFlags extends basecommand_1.BaseCommand {
19
+ static description = 'Upload an extracted set of feature flags';
20
+ static examples = [
21
+ `$ contrail feature-flags import
22
+ `,
23
+ ];
24
+ static flags = {
25
+ ...basecommand_1.BaseCommand.baseFlags,
26
+ dir: core_1.Flags.string({
27
+ char: 'd',
28
+ description: `The directory from which to import the feature flags. Defaults to the current directory. The feature flags will be loaded from $dir/${export_1.FEATURE_FLAGS_FILE}`,
29
+ required: false,
30
+ default: '.',
31
+ }),
32
+ };
33
+ async run() {
34
+ try {
35
+ await (0, login_1.authUser)();
36
+ await assertCanManageFeatureFlags();
37
+ this.log(`${chalk_1.default.blue.bold('START')} Uploading Feature Flags.`);
38
+ this.log(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Uploading`);
39
+ await uploadFeatureFlags(this.flags.dir);
40
+ this.log(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Uploaded`);
41
+ }
42
+ catch (e) {
43
+ this.log(`${chalk_1.default.red.bold('FAIL')} Feature Flags Upload Failed. ${(0, error_helpers_1.describeError)(e)}`);
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+ this.log(`${chalk_1.default.green.bold('COMPLETE')} Feature Flags Upload Complete.`);
48
+ }
49
+ }
50
+ exports.default = ImportFeatureFlags;
51
+ /**
52
+ * The feature flags API only lets a platform admin/manager create or delete flags, and a
53
+ * missing role surfaces as a generic 403 partway through the run. Checking up front stops
54
+ * the command before it half-applies the file.
55
+ */
56
+ async function assertCanManageFeatureFlags() {
57
+ let platformRoles;
58
+ try {
59
+ const currentUser = await (0, users_1.getCurrentUserWithPlatformRoles)();
60
+ platformRoles = normalizePlatformRoles(currentUser?.platformRoles);
61
+ }
62
+ catch (e) {
63
+ throw new Error(`Could not read your platform roles. ${(0, error_helpers_1.describeError)(e)}`);
64
+ }
65
+ if (!platformRoles.some((role) => exports.REQUIRED_PLATFORM_ROLES.includes(role))) {
66
+ throw new Error(`Importing feature flags requires the ${exports.REQUIRED_PLATFORM_ROLES.join(' or ')} platform role. ` +
67
+ `Your roles: ${platformRoles.length ? platformRoles.join(', ') : 'none'}.`);
68
+ }
69
+ }
70
+ exports.assertCanManageFeatureFlags = assertCanManageFeatureFlags;
71
+ /**
72
+ * Makes the target org's enabled feature flags match $dir/feature-flags.yml: enables every
73
+ * flag in the file and disables any flag the target has that the file does not, the same
74
+ * way groups import reconciles groups. Feature flags are not editable in place, so there is
75
+ * no update step.
76
+ *
77
+ * Failures are collected and rethrown so an import never reports success while the org is
78
+ * missing flags.
79
+ */
80
+ async function uploadFeatureFlags(dir) {
81
+ const filePath = path.join(dir, export_1.FEATURE_FLAGS_FILE);
82
+ if (!fs_1.default.existsSync(filePath)) {
83
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} No feature flags found in ${dir}. Skipping.`);
84
+ return;
85
+ }
86
+ const data = (0, file_helpers_1.loadData)(filePath);
87
+ if (!Array.isArray(data)) {
88
+ throw new Error(`${filePath} must contain a YAML list of feature flags`);
89
+ }
90
+ const featureNamesToEnable = new Set(data.map(toFeatureName).filter(Boolean));
91
+ const existingFlags = (await (0, feature_flags_1.getFeatureFlags)()) ?? [];
92
+ const enabledFeatureNames = new Set(existingFlags.map((flag) => flag?.featureName));
93
+ const toCreate = [...featureNamesToEnable].filter((name) => !enabledFeatureNames.has(name));
94
+ const toDelete = existingFlags.filter((flag) => !featureNamesToEnable.has(flag?.featureName));
95
+ const failures = [];
96
+ for (const featureName of toCreate) {
97
+ try {
98
+ await (0, feature_flags_1.createFeatureFlag)(featureName);
99
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Enabled ${featureName}`);
100
+ }
101
+ catch (e) {
102
+ failures.push(`could not enable ${featureName}: ${(0, error_helpers_1.describeError)(e)}`);
103
+ }
104
+ }
105
+ const removed = [];
106
+ for (const flag of toDelete) {
107
+ try {
108
+ await (0, feature_flags_1.deleteFeatureFlag)(flag.id);
109
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Disabled ${flag.featureName}`);
110
+ removed.push(flag.featureName);
111
+ }
112
+ catch (e) {
113
+ failures.push(`could not disable ${flag.featureName}: ${(0, error_helpers_1.describeError)(e)}`);
114
+ }
115
+ }
116
+ // call out the removals explicitly: they are the only destructive part of the import
117
+ if (removed.length) {
118
+ const removedList = removed.sort().join(', ');
119
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} The following ${removed.length} flag(s) were ` +
120
+ `enabled in this org but not in the source, so they were removed: ${removedList}`);
121
+ }
122
+ if (failures.length) {
123
+ throw new Error(`Failed to apply feature flags. ${failures.join('; ')}`);
124
+ }
125
+ }
126
+ exports.uploadFeatureFlags = uploadFeatureFlags;
127
+ // the API returns platformRoles as an array, but some contexts carry it as a JSON string
128
+ function normalizePlatformRoles(platformRoles) {
129
+ if (Array.isArray(platformRoles)) {
130
+ return platformRoles;
131
+ }
132
+ if (typeof platformRoles === 'string') {
133
+ try {
134
+ const parsed = JSON.parse(platformRoles);
135
+ return Array.isArray(parsed) ? parsed : [];
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ return [];
142
+ }
143
+ // entries are exported as `- featureName: X`, but a hand written `- X` is accepted too
144
+ function toFeatureName(entry) {
145
+ const featureName = typeof entry === 'string' ? entry : entry?.featureName;
146
+ if (typeof featureName !== 'string' || !featureName.trim()) {
147
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Skipping entry with no featureName`);
148
+ return undefined;
149
+ }
150
+ return featureName.trim();
151
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs_1 = tslib_1.__importDefault(require("fs"));
5
+ const import_1 = require("./import");
6
+ const file_helpers_1 = require("../../helpers/file-helpers");
7
+ const feature_flags_1 = require("../../api/feature-flags");
8
+ const users_1 = require("../../api/users");
9
+ const logger_1 = tslib_1.__importDefault(require("../../logger"));
10
+ jest.mock('../../helpers/file-helpers');
11
+ jest.mock('../../api/feature-flags');
12
+ jest.mock('../../api/users');
13
+ const mocked = (fn) => fn;
14
+ describe('assertCanManageFeatureFlags', () => {
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ });
18
+ it.each(['PLATFORM_ADMIN', 'PLATFORM_MANAGER'])('allows a %s', async (role) => {
19
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({ platformRoles: [role] });
20
+ await expect((0, import_1.assertCanManageFeatureFlags)()).resolves.toBeUndefined();
21
+ });
22
+ it('accepts platformRoles serialized as a JSON string', async () => {
23
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({
24
+ platformRoles: '["PLATFORM_MANAGER"]',
25
+ });
26
+ await expect((0, import_1.assertCanManageFeatureFlags)()).resolves.toBeUndefined();
27
+ });
28
+ it('rejects a user with no platform role', async () => {
29
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({ platformRoles: [] });
30
+ await expect((0, import_1.assertCanManageFeatureFlags)()).rejects.toThrow('requires the PLATFORM_ADMIN or PLATFORM_MANAGER platform role');
31
+ });
32
+ it('rejects a user holding only some other platform role', async () => {
33
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({
34
+ platformRoles: ['PLATFORM_USER'],
35
+ });
36
+ await expect((0, import_1.assertCanManageFeatureFlags)()).rejects.toThrow('Your roles: PLATFORM_USER.');
37
+ });
38
+ it('rejects when the roles could not be read', async () => {
39
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockRejectedValue(new Error('Forbidden resource'));
40
+ await expect((0, import_1.assertCanManageFeatureFlags)()).rejects.toThrow('Could not read your platform roles. Forbidden resource');
41
+ });
42
+ });
43
+ describe('uploadFeatureFlags', () => {
44
+ let existsSyncSpy;
45
+ beforeEach(() => {
46
+ jest.clearAllMocks();
47
+ existsSyncSpy = jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(true);
48
+ mocked(feature_flags_1.createFeatureFlag).mockResolvedValue({});
49
+ mocked(feature_flags_1.deleteFeatureFlag).mockResolvedValue({});
50
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([]);
51
+ });
52
+ afterEach(() => {
53
+ existsSyncSpy.mockRestore();
54
+ });
55
+ it('skips when the export has no feature-flags.yml', async () => {
56
+ existsSyncSpy.mockReturnValue(false);
57
+ await (0, import_1.uploadFeatureFlags)('export-dir');
58
+ expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
59
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
60
+ });
61
+ it('enables every flag from the export that the target org is missing', async () => {
62
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }, { featureName: 'PROMOTE' }]);
63
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
64
+ await (0, import_1.uploadFeatureFlags)('export-dir');
65
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledTimes(1);
66
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledWith('PROMOTE');
67
+ });
68
+ it('disables flags the target org has that the export does not, so the orgs match', async () => {
69
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
70
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
71
+ { id: '1', featureName: 'SHOWROOM' },
72
+ { id: '2', featureName: 'DEV_TABS' },
73
+ ]);
74
+ await (0, import_1.uploadFeatureFlags)('export-dir');
75
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
76
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).toHaveBeenCalledTimes(1);
77
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).toHaveBeenCalledWith('2');
78
+ });
79
+ it('summarizes what it removed, and why, at the end of the run', async () => {
80
+ const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
81
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
82
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
83
+ { id: '1', featureName: 'SHOWROOM' },
84
+ { id: '2', featureName: 'PROMOTE' },
85
+ { id: '3', featureName: 'DEV_TABS' },
86
+ ]);
87
+ await (0, import_1.uploadFeatureFlags)('export-dir');
88
+ const summary = infoSpy.mock.calls.map(([message]) => String(message)).at(-1);
89
+ expect(summary).toContain('2 flag(s) were enabled in this org but not in the source');
90
+ expect(summary).toContain('DEV_TABS, PROMOTE');
91
+ infoSpy.mockRestore();
92
+ });
93
+ it('does not print a removal summary when nothing was removed', async () => {
94
+ const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
95
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
96
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
97
+ await (0, import_1.uploadFeatureFlags)('export-dir');
98
+ const messages = infoSpy.mock.calls.map(([message]) => String(message));
99
+ expect(messages.some((message) => message.includes('so they were removed'))).toBe(false);
100
+ infoSpy.mockRestore();
101
+ });
102
+ it('accepts plain feature names and ignores entries with no featureName', async () => {
103
+ mocked(file_helpers_1.loadData).mockReturnValue(['SHOWROOM', { featureName: ' PROMOTE ' }, {}, null]);
104
+ await (0, import_1.uploadFeatureFlags)('export-dir');
105
+ expect(mocked(feature_flags_1.createFeatureFlag).mock.calls.map(([name]) => name)).toEqual([
106
+ 'SHOWROOM',
107
+ 'PROMOTE',
108
+ ]);
109
+ });
110
+ it('rejects when the file is not a list', async () => {
111
+ mocked(file_helpers_1.loadData).mockReturnValue({ featureName: 'SHOWROOM' });
112
+ await expect((0, import_1.uploadFeatureFlags)('export-dir')).rejects.toThrow('must contain a YAML list of feature flags');
113
+ });
114
+ it('applies what it can and then rejects, rather than reporting success', async () => {
115
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }, { featureName: 'PROMOTE' }]);
116
+ mocked(feature_flags_1.createFeatureFlag).mockImplementation(async (featureName) => {
117
+ if (featureName === 'SHOWROOM') {
118
+ throw new Error('Forbidden resource');
119
+ }
120
+ return {};
121
+ });
122
+ await expect((0, import_1.uploadFeatureFlags)('export-dir')).rejects.toThrow('could not enable SHOWROOM: Forbidden resource');
123
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledWith('PROMOTE');
124
+ });
125
+ });
@@ -2,11 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exportGroups = exports.DEFAULT_FILE_LOCATION = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const path = tslib_1.__importStar(require("path"));
6
5
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
- const fs_1 = tslib_1.__importDefault(require("fs"));
8
6
  const login_1 = require("../../auth/login");
9
- const js_yaml_1 = tslib_1.__importDefault(require("js-yaml"));
10
7
  const core_1 = require("@oclif/core");
11
8
  const file_helpers_1 = require("../../helpers/file-helpers");
12
9
  const users_1 = require("../../api/users");
@@ -61,18 +58,9 @@ async function exportGroups(fileLocation, overwrite) {
61
58
  });
62
59
  }
63
60
  groups = groups.sort((one, two) => (one.name < two.name ? -1 : 1));
64
- await saveData(groups, fileLocation, overwrite);
61
+ await (0, file_helpers_1.saveData)(groups, fileLocation, overwrite);
65
62
  }
66
63
  exports.exportGroups = exportGroups;
67
- async function saveData(data, fileLocation, overwrite) {
68
- const yamlStr = js_yaml_1.default.dump(data);
69
- const CURRENT_DIR = process.cwd();
70
- const pathToWrite = path.join(CURRENT_DIR, fileLocation);
71
- if (fs_1.default.existsSync(pathToWrite)) {
72
- await (0, file_helpers_1.safeFileOverwrite)(pathToWrite, overwrite);
73
- }
74
- fs_1.default.writeFileSync(pathToWrite, yamlStr, 'utf8');
75
- }
76
64
  function getGroupPrincipals(group, allGroups, orgUsers) {
77
65
  if (!group?.groupPrincipals) {
78
66
  return [];
@@ -2,13 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.uploadGroups = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const path = tslib_1.__importStar(require("path"));
6
5
  const sdk_1 = require("@contrail/sdk");
7
6
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
8
- const fs_1 = tslib_1.__importDefault(require("fs"));
9
7
  const login_1 = require("../../auth/login");
10
- const js_yaml_1 = tslib_1.__importDefault(require("js-yaml"));
11
8
  const core_1 = require("@oclif/core");
9
+ const file_helpers_1 = require("../../helpers/file-helpers");
12
10
  const basecommand_1 = require("../../basecommand");
13
11
  const logger_1 = tslib_1.__importDefault(require("../../logger"));
14
12
  class ImportGroups extends basecommand_1.BaseCommand {
@@ -39,23 +37,12 @@ class ImportGroups extends basecommand_1.BaseCommand {
39
37
  }
40
38
  }
41
39
  exports.default = ImportGroups;
42
- function getGroupsFile(fileLocation) {
43
- const CURRENT_DIR = process.cwd();
44
- try {
45
- const fullPath = path.join(CURRENT_DIR, fileLocation);
46
- const doc = js_yaml_1.default.load(fs_1.default.readFileSync(fullPath, 'utf8'));
47
- return doc;
48
- }
49
- catch (e) {
50
- throw new Error(`ERROR PARSING YAML: ${e}`);
51
- }
52
- }
53
40
  async function uploadGroups(fileLocation) {
54
41
  const existingGroups = await new sdk_1.Entities().get({
55
42
  entityName: 'group',
56
43
  relations: ['groupPrincipals'],
57
44
  });
58
- const groupsToImport = getGroupsFile(fileLocation);
45
+ const groupsToImport = (0, file_helpers_1.loadData)(fileLocation);
59
46
  const orgUsers = await new sdk_1.Entities().get({
60
47
  entityName: 'user-org',
61
48
  });
@@ -9,3 +9,17 @@ export default class ImportInstance extends BaseCommand {
9
9
  };
10
10
  run(): Promise<void>;
11
11
  }
12
+ /**
13
+ * Imports an instance export in dependency order:
14
+ * 1. option sets, types, option set hierarchies, rule sets
15
+ * 2. users and groups (policy principals)
16
+ * 3. type property policies and type policies
17
+ * 4. type overrides — after policies, because they reference policies, rule sets and option
18
+ * set hierarchies by slug and the API rejects the whole upload if any of them is missing
19
+ * 5. org config
20
+ *
21
+ * Fails (rejects) if the type overrides could not be applied, so the import does not
22
+ * report success while the org is missing customizations carried by overrides
23
+ * (labels/formulas/editability of core properties, subtype-level property changes).
24
+ */
25
+ export declare function importInstance(dir: string): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.importInstance = void 0;
3
4
  const tslib_1 = require("tslib");
4
5
  const path = tslib_1.__importStar(require("path"));
5
6
  const core_1 = require("@oclif/core");
@@ -8,6 +9,8 @@ const import_1 = require("../users/import");
8
9
  const import_2 = require("../groups/import");
9
10
  const uploadTypePropertyPolicies_1 = require("../types/uploadTypePropertyPolicies");
10
11
  const uploadTypePolicies_1 = require("../types/uploadTypePolicies");
12
+ const uploadTypeOverrides_1 = require("../types/uploadTypeOverrides");
13
+ const import_3 = require("../org-config/import");
11
14
  const basecommand_1 = require("../../basecommand");
12
15
  class ImportInstance extends basecommand_1.BaseCommand {
13
16
  static description = 'Import the data to an instance, including types and users';
@@ -25,11 +28,30 @@ class ImportInstance extends basecommand_1.BaseCommand {
25
28
  }),
26
29
  };
27
30
  async run() {
28
- await (0, loadAll_1.loadAll)(this.flags.dir, true);
29
- await (0, import_1.uploadUsers)(path.join(this.flags.dir, 'users.yml'));
30
- await (0, import_2.uploadGroups)(path.join(this.flags.dir, 'groups.yml'));
31
- await (0, uploadTypePropertyPolicies_1.uploadTypePropertyPolicies)(this.flags.dir);
32
- await (0, uploadTypePolicies_1.uploadTypePolicies)(this.flags.dir);
31
+ await importInstance(this.flags.dir);
33
32
  }
34
33
  }
35
34
  exports.default = ImportInstance;
35
+ /**
36
+ * Imports an instance export in dependency order:
37
+ * 1. option sets, types, option set hierarchies, rule sets
38
+ * 2. users and groups (policy principals)
39
+ * 3. type property policies and type policies
40
+ * 4. type overrides — after policies, because they reference policies, rule sets and option
41
+ * set hierarchies by slug and the API rejects the whole upload if any of them is missing
42
+ * 5. org config
43
+ *
44
+ * Fails (rejects) if the type overrides could not be applied, so the import does not
45
+ * report success while the org is missing customizations carried by overrides
46
+ * (labels/formulas/editability of core properties, subtype-level property changes).
47
+ */
48
+ async function importInstance(dir) {
49
+ await (0, loadAll_1.loadAll)(dir, true, false);
50
+ await (0, import_1.uploadUsers)(path.join(dir, 'users.yml'));
51
+ await (0, import_2.uploadGroups)(path.join(dir, 'groups.yml'));
52
+ await (0, uploadTypePropertyPolicies_1.uploadTypePropertyPolicies)(dir);
53
+ await (0, uploadTypePolicies_1.uploadTypePolicies)(dir);
54
+ await (0, uploadTypeOverrides_1.uploadTypeOverrides)(dir);
55
+ await (0, import_3.uploadOrgConfig)(dir);
56
+ }
57
+ exports.importInstance = importInstance;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const import_1 = require("./import");
4
+ const loadAll_1 = require("../types/loadAll");
5
+ const import_2 = require("../users/import");
6
+ const import_3 = require("../groups/import");
7
+ const uploadTypePropertyPolicies_1 = require("../types/uploadTypePropertyPolicies");
8
+ const uploadTypePolicies_1 = require("../types/uploadTypePolicies");
9
+ const uploadTypeOverrides_1 = require("../types/uploadTypeOverrides");
10
+ const import_4 = require("../org-config/import");
11
+ jest.mock('../types/loadAll');
12
+ jest.mock('../users/import');
13
+ jest.mock('../groups/import');
14
+ jest.mock('../types/uploadTypePropertyPolicies');
15
+ jest.mock('../types/uploadTypePolicies');
16
+ jest.mock('../types/uploadTypeOverrides');
17
+ jest.mock('../org-config/import');
18
+ const mocked = (fn) => fn;
19
+ const STEPS = [
20
+ loadAll_1.loadAll,
21
+ import_2.uploadUsers,
22
+ import_3.uploadGroups,
23
+ uploadTypePropertyPolicies_1.uploadTypePropertyPolicies,
24
+ uploadTypePolicies_1.uploadTypePolicies,
25
+ uploadTypeOverrides_1.uploadTypeOverrides,
26
+ import_4.uploadOrgConfig,
27
+ ];
28
+ describe('importInstance', () => {
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ for (const fn of STEPS) {
32
+ mocked(fn).mockResolvedValue(undefined);
33
+ }
34
+ });
35
+ it('skips policies/overrides/org-config in loadAll and uploads them after users and groups', async () => {
36
+ await (0, import_1.importInstance)('export-dir');
37
+ expect(mocked(loadAll_1.loadAll)).toHaveBeenCalledWith('export-dir', true, false);
38
+ expect(mocked(uploadTypeOverrides_1.uploadTypeOverrides)).toHaveBeenCalledWith('export-dir');
39
+ expect(mocked(import_4.uploadOrgConfig)).toHaveBeenCalledWith('export-dir');
40
+ const order = STEPS.map((fn) => mocked(fn).mock.invocationCallOrder[0]);
41
+ // strictly increasing => called in exactly this order
42
+ for (let i = 1; i < order.length; i++) {
43
+ expect(order[i]).toBeGreaterThan(order[i - 1]);
44
+ }
45
+ });
46
+ it('uploads type overrides after type policies (CR-2095)', async () => {
47
+ await (0, import_1.importInstance)('export-dir');
48
+ const policiesOrder = mocked(uploadTypePolicies_1.uploadTypePolicies).mock.invocationCallOrder[0];
49
+ const overridesOrder = mocked(uploadTypeOverrides_1.uploadTypeOverrides).mock.invocationCallOrder[0];
50
+ expect(overridesOrder).toBeGreaterThan(policiesOrder);
51
+ });
52
+ it('rejects when the type override upload fails instead of reporting success', async () => {
53
+ mocked(uploadTypeOverrides_1.uploadTypeOverrides).mockRejectedValue(new Error('policy with identifier X not found'));
54
+ await expect((0, import_1.importInstance)('export-dir')).rejects.toThrow('policy with identifier X not found');
55
+ expect(mocked(import_4.uploadOrgConfig)).not.toHaveBeenCalled();
56
+ });
57
+ });
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../basecommand';
2
+ export default class ExportOrgConfig extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ outDir: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
+ overwrite: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
+ alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
+ logLevel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
13
+ export declare function exportOrgConfig(outDir: string, overwrite: boolean): Promise<void>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportOrgConfig = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const path = tslib_1.__importStar(require("path"));
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const fs_1 = tslib_1.__importDefault(require("fs"));
8
+ const login_1 = require("../../auth/login");
9
+ const core_1 = require("@oclif/core");
10
+ const file_helpers_1 = require("../../helpers/file-helpers");
11
+ const org_config_1 = require("../../api/org-config");
12
+ const basecommand_1 = require("../../basecommand");
13
+ const logger_1 = tslib_1.__importDefault(require("../../logger"));
14
+ const ORG_SPECIFIC_FIELDS = [
15
+ 'id',
16
+ 'orgId',
17
+ 'createdOn',
18
+ 'updatedOn',
19
+ 'createdById',
20
+ 'updatedById',
21
+ 'createdBy',
22
+ 'updatedBy',
23
+ ];
24
+ class ExportOrgConfig extends basecommand_1.BaseCommand {
25
+ static description = 'Export the org config for your Org';
26
+ static examples = [
27
+ `$ contrail org-config export
28
+ `,
29
+ ];
30
+ static flags = {
31
+ ...basecommand_1.BaseCommand.baseFlags,
32
+ outDir: core_1.Flags.string({
33
+ char: 'd',
34
+ description: 'The directory to which to export the org config data. Defaults to the current directory',
35
+ required: false,
36
+ default: '.',
37
+ }),
38
+ overwrite: core_1.Flags.boolean({
39
+ char: 'f',
40
+ description: 'Replaces any existing export files in outDir',
41
+ required: false,
42
+ default: false,
43
+ }),
44
+ };
45
+ async run() {
46
+ try {
47
+ await (0, login_1.authUser)();
48
+ this.log(`${chalk_1.default.blue.bold('START')} Getting Org Config.`);
49
+ this.log(`${chalk_1.default.yellow.bold('ORG CONFIG')} Exporting`);
50
+ await exportOrgConfig(this.flags.outDir, this.flags.overwrite);
51
+ this.log(`${chalk_1.default.yellow.bold('ORG CONFIG')} Exported`);
52
+ }
53
+ catch (e) {
54
+ this.log(`${chalk_1.default.red.bold('FAIL')} Org Config Export Failed. ${e}`);
55
+ return;
56
+ }
57
+ this.log(`${chalk_1.default.green.bold('COMPLETE')} Org Config Export Complete.`);
58
+ }
59
+ }
60
+ exports.default = ExportOrgConfig;
61
+ async function exportOrgConfig(outDir, overwrite) {
62
+ const orgConfigs = await (0, org_config_1.getOrgConfigs)();
63
+ if (!orgConfigs?.length) {
64
+ logger_1.default.info(`${chalk_1.default.yellow.bold('ORG CONFIG')} No org config found. Skipping.`);
65
+ return;
66
+ }
67
+ const orgConfig = { ...orgConfigs[0] };
68
+ for (const field of ORG_SPECIFIC_FIELDS) {
69
+ delete orgConfig[field];
70
+ }
71
+ fs_1.default.mkdirSync(path.resolve(outDir), { recursive: true });
72
+ await (0, file_helpers_1.saveData)(orgConfig, path.join(outDir, 'org-config.yml'), overwrite);
73
+ }
74
+ exports.exportOrgConfig = exportOrgConfig;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../basecommand';
2
+ export default class ImportOrgConfig extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ dir: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
+ alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
8
+ logLevel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
12
+ export declare function uploadOrgConfig(dir: string): Promise<void>;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadOrgConfig = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const path = tslib_1.__importStar(require("path"));
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const fs_1 = tslib_1.__importDefault(require("fs"));
8
+ const login_1 = require("../../auth/login");
9
+ const core_1 = require("@oclif/core");
10
+ const file_helpers_1 = require("../../helpers/file-helpers");
11
+ const org_config_1 = require("../../api/org-config");
12
+ const basecommand_1 = require("../../basecommand");
13
+ const logger_1 = tslib_1.__importDefault(require("../../logger"));
14
+ class ImportOrgConfig extends basecommand_1.BaseCommand {
15
+ static description = 'Upload an extracted org config';
16
+ static examples = [
17
+ `$ contrail org-config import
18
+ `,
19
+ ];
20
+ static flags = {
21
+ ...basecommand_1.BaseCommand.baseFlags,
22
+ dir: core_1.Flags.string({
23
+ char: 'd',
24
+ description: 'The directory from which to import the org config data. Defaults to the current directory. The org config data will be loaded from $dir/org-config.yml',
25
+ required: false,
26
+ default: '.',
27
+ }),
28
+ };
29
+ async run() {
30
+ try {
31
+ await (0, login_1.authUser)();
32
+ this.log(`${chalk_1.default.blue.bold('START')} Uploading Org Config.`);
33
+ this.log(`${chalk_1.default.yellow.bold('ORG CONFIG')} Uploading`);
34
+ await uploadOrgConfig(this.flags.dir);
35
+ this.log(`${chalk_1.default.yellow.bold('ORG CONFIG')} Uploaded`);
36
+ }
37
+ catch (e) {
38
+ this.log(`${chalk_1.default.red.bold('FAIL')} Org Config Upload Failed. ${e}`);
39
+ return;
40
+ }
41
+ this.log(`${chalk_1.default.green.bold('COMPLETE')} Org Config Upload Complete.`);
42
+ }
43
+ }
44
+ exports.default = ImportOrgConfig;
45
+ async function uploadOrgConfig(dir) {
46
+ const filePath = path.join(dir, 'org-config.yml');
47
+ if (!fs_1.default.existsSync(filePath)) {
48
+ logger_1.default.info(`${chalk_1.default.yellow.bold('ORG CONFIG')} No org config found in ${dir}. Skipping.`);
49
+ return;
50
+ }
51
+ const orgConfig = (0, file_helpers_1.loadData)(filePath);
52
+ if (!orgConfig || typeof orgConfig !== 'object' || Array.isArray(orgConfig)) {
53
+ throw new Error(`${filePath} must contain a single YAML object`);
54
+ }
55
+ const existingOrgConfigs = await (0, org_config_1.getOrgConfigs)();
56
+ if (existingOrgConfigs?.length) {
57
+ await (0, org_config_1.updateOrgConfig)(existingOrgConfigs[0].id, orgConfig);
58
+ }
59
+ else {
60
+ await (0, org_config_1.createOrgConfig)(orgConfig);
61
+ }
62
+ }
63
+ exports.uploadOrgConfig = uploadOrgConfig;