@contrail/cli 3.5.4-alpha.f60c4df → 3.5.5-alpha.177136f

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 (65) 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 +42 -0
  13. package/dist/commands/feature-flags/import.js +191 -0
  14. package/dist/commands/feature-flags/import.spec.d.ts +1 -0
  15. package/dist/commands/feature-flags/import.spec.js +194 -0
  16. package/dist/commands/groups/export.js +1 -13
  17. package/dist/commands/groups/import.js +2 -15
  18. package/dist/commands/instance/export.d.ts +9 -0
  19. package/dist/commands/instance/export.js +18 -3
  20. package/dist/commands/instance/export.spec.d.ts +1 -0
  21. package/dist/commands/instance/export.spec.js +32 -0
  22. package/dist/commands/instance/import.d.ts +17 -0
  23. package/dist/commands/instance/import.js +32 -5
  24. package/dist/commands/instance/import.spec.d.ts +1 -0
  25. package/dist/commands/instance/import.spec.js +61 -0
  26. package/dist/commands/org-config/export.d.ts +13 -0
  27. package/dist/commands/org-config/export.js +74 -0
  28. package/dist/commands/org-config/import.d.ts +12 -0
  29. package/dist/commands/org-config/import.js +63 -0
  30. package/dist/commands/types/getAll.js +2 -0
  31. package/dist/commands/types/loadAll.d.ts +13 -1
  32. package/dist/commands/types/loadAll.js +41 -4
  33. package/dist/commands/types/loadAll.spec.d.ts +1 -0
  34. package/dist/commands/types/loadAll.spec.js +77 -0
  35. package/dist/commands/types/mergeOptionSets.js +2 -9
  36. package/dist/commands/types/mergeTypeProperties.js +2 -6
  37. package/dist/commands/types/parseOptionSetsLocalFiles.js +3 -7
  38. package/dist/commands/types/uploadTypeOverrides.d.ts +10 -0
  39. package/dist/commands/types/uploadTypeOverrides.js +19 -8
  40. package/dist/commands/types/uploadTypeOverrides.spec.d.ts +1 -0
  41. package/dist/commands/types/uploadTypeOverrides.spec.js +60 -0
  42. package/dist/commands/users/export.js +1 -13
  43. package/dist/commands/users/import.js +2 -15
  44. package/dist/commands/workflow-templates/export.d.ts +2 -3
  45. package/dist/commands/workflow-templates/export.js +23 -122
  46. package/dist/commands/workflow-templates/load.d.ts +0 -2
  47. package/dist/commands/workflow-templates/load.js +6 -63
  48. package/dist/helpers/app/app-config-validators.d.ts +5 -0
  49. package/dist/helpers/app/app-config-validators.js +102 -3
  50. package/dist/helpers/app/app-config-validators.spec.js +46 -0
  51. package/dist/helpers/app/extensions-deployer.d.ts +10 -2
  52. package/dist/helpers/app/extensions-deployer.js +31 -37
  53. package/dist/helpers/app/extensions-deployer.spec.d.ts +1 -0
  54. package/dist/helpers/app/extensions-deployer.spec.js +29 -0
  55. package/dist/helpers/error-helpers.d.ts +9 -0
  56. package/dist/helpers/error-helpers.js +35 -0
  57. package/dist/helpers/file-helpers.d.ts +2 -0
  58. package/dist/helpers/file-helpers.js +25 -1
  59. package/dist/helpers/projects/helpers.js +2 -6
  60. package/dist/helpers/types/type-parser.js +9 -15
  61. package/dist/helpers/workflow-templates/helper.d.ts +1 -2
  62. package/dist/helpers/workflow-templates/helper.js +6 -36
  63. package/dist/interfaces.d.ts +37 -1
  64. package/oclif.manifest.json +236 -30
  65. package/package.json +7 -1
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadFeatureFlags = exports.uploadFeatureFlagsIfPermitted = 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
+ async function getPlatformRoles() {
52
+ try {
53
+ const currentUser = await (0, users_1.getCurrentUserWithPlatformRoles)();
54
+ return normalizePlatformRoles(currentUser?.platformRoles);
55
+ }
56
+ catch (e) {
57
+ throw new Error(`Could not read your platform roles. ${(0, error_helpers_1.describeError)(e)}`);
58
+ }
59
+ }
60
+ /**
61
+ * The feature flags API only lets a platform admin/manager create or delete flags, and a
62
+ * missing role surfaces as a generic 403 partway through the run. Checking up front stops
63
+ * the command before it half-applies the file.
64
+ */
65
+ async function assertCanManageFeatureFlags() {
66
+ const platformRoles = await getPlatformRoles();
67
+ if (!platformRoles.some((role) => exports.REQUIRED_PLATFORM_ROLES.includes(role))) {
68
+ throw new Error(`Importing feature flags requires the ${exports.REQUIRED_PLATFORM_ROLES.join(' or ')} platform role. ` +
69
+ `Your roles: ${platformRoles.length ? platformRoles.join(', ') : 'none'}.`);
70
+ }
71
+ }
72
+ exports.assertCanManageFeatureFlags = assertCanManageFeatureFlags;
73
+ /**
74
+ * The `instance import` variant: feature flags are one step of a much larger config copy that
75
+ * org admins without a platform role are expected to run, so a missing role (or roles we could
76
+ * not read) logs and skips instead of failing the whole import. Errors from the flag upload
77
+ * itself still propagate — only the permission check is soft here.
78
+ *
79
+ * `contrail feature-flags import` deliberately does the opposite and fails, because there the
80
+ * flags are the entire point of the command.
81
+ */
82
+ async function uploadFeatureFlagsIfPermitted(dir) {
83
+ let platformRoles;
84
+ try {
85
+ platformRoles = await getPlatformRoles();
86
+ }
87
+ catch (e) {
88
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Skipping feature flags: ${(0, error_helpers_1.describeError)(e)} ` +
89
+ `Ask a platform admin to run 'contrail feature-flags import -d ${dir}'.`);
90
+ return;
91
+ }
92
+ if (!platformRoles.some((role) => exports.REQUIRED_PLATFORM_ROLES.includes(role))) {
93
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Skipping feature flags: this requires the ` +
94
+ `${exports.REQUIRED_PLATFORM_ROLES.join(' or ')} platform role and you have ` +
95
+ `${platformRoles.length ? platformRoles.join(', ') : 'none'}. ` +
96
+ `Ask a platform admin to run 'contrail feature-flags import -d ${dir}'.`);
97
+ return;
98
+ }
99
+ await uploadFeatureFlags(dir);
100
+ }
101
+ exports.uploadFeatureFlagsIfPermitted = uploadFeatureFlagsIfPermitted;
102
+ /**
103
+ * Makes the target org's enabled feature flags match $dir/feature-flags.yml: enables every
104
+ * flag in the file and disables any flag the target has that the file does not, the same
105
+ * way groups import reconciles groups. Feature flags are not editable in place, so there is
106
+ * no update step.
107
+ *
108
+ * Failures are collected and rethrown so an import never reports success while the org is
109
+ * missing flags.
110
+ *
111
+ * A file that lists no flags at all is skipped rather than applied: reconciling against an
112
+ * empty list would disable every flag in the target org, which is almost never what a copy
113
+ * is meant to do. Flags that should be off in the target have to be disabled by hand.
114
+ */
115
+ async function uploadFeatureFlags(dir) {
116
+ const filePath = path.join(dir, export_1.FEATURE_FLAGS_FILE);
117
+ if (!fs_1.default.existsSync(filePath)) {
118
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} No feature flags found in ${dir}. Skipping.`);
119
+ return;
120
+ }
121
+ const data = (0, file_helpers_1.loadData)(filePath) ?? [];
122
+ if (!Array.isArray(data)) {
123
+ throw new Error(`${filePath} must contain a YAML list of feature flags`);
124
+ }
125
+ const featureNamesToEnable = new Set(data.map(toFeatureName).filter(Boolean));
126
+ if (featureNamesToEnable.size === 0) {
127
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} ${filePath} lists no feature flags. Skipping, so ` +
128
+ 'the flags currently enabled in this org are left as they are.');
129
+ return;
130
+ }
131
+ const existingFlags = (await (0, feature_flags_1.getFeatureFlags)()) ?? [];
132
+ const enabledFeatureNames = new Set(existingFlags.map((flag) => flag?.featureName));
133
+ const toCreate = [...featureNamesToEnable].filter((name) => !enabledFeatureNames.has(name));
134
+ const toDelete = existingFlags.filter((flag) => !featureNamesToEnable.has(flag?.featureName));
135
+ const failures = [];
136
+ for (const featureName of toCreate) {
137
+ try {
138
+ await (0, feature_flags_1.createFeatureFlag)(featureName);
139
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Enabled ${featureName}`);
140
+ }
141
+ catch (e) {
142
+ failures.push(`could not enable ${featureName}: ${(0, error_helpers_1.describeError)(e)}`);
143
+ }
144
+ }
145
+ const removed = [];
146
+ for (const flag of toDelete) {
147
+ try {
148
+ await (0, feature_flags_1.deleteFeatureFlag)(flag.id);
149
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Disabled ${flag.featureName}`);
150
+ removed.push(flag.featureName);
151
+ }
152
+ catch (e) {
153
+ failures.push(`could not disable ${flag.featureName}: ${(0, error_helpers_1.describeError)(e)}`);
154
+ }
155
+ }
156
+ // call out the removals explicitly: they are the only destructive part of the import
157
+ if (removed.length) {
158
+ const removedList = removed.sort().join(', ');
159
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} The following ${removed.length} flag(s) were ` +
160
+ `enabled in this org but not in the source, so they were removed: ${removedList}`);
161
+ }
162
+ if (failures.length) {
163
+ throw new Error(`Failed to apply feature flags. ${failures.join('; ')}`);
164
+ }
165
+ }
166
+ exports.uploadFeatureFlags = uploadFeatureFlags;
167
+ // the API returns platformRoles as an array, but some contexts carry it as a JSON string
168
+ function normalizePlatformRoles(platformRoles) {
169
+ if (Array.isArray(platformRoles)) {
170
+ return platformRoles;
171
+ }
172
+ if (typeof platformRoles === 'string') {
173
+ try {
174
+ const parsed = JSON.parse(platformRoles);
175
+ return Array.isArray(parsed) ? parsed : [];
176
+ }
177
+ catch {
178
+ return [];
179
+ }
180
+ }
181
+ return [];
182
+ }
183
+ // entries are exported as `- featureName: X`, but a hand written `- X` is accepted too
184
+ function toFeatureName(entry) {
185
+ const featureName = typeof entry === 'string' ? entry : entry?.featureName;
186
+ if (typeof featureName !== 'string' || !featureName.trim()) {
187
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} Skipping entry with no featureName`);
188
+ return undefined;
189
+ }
190
+ return featureName.trim();
191
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,194 @@
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('skips an export that lists no flags instead of disabling everything in the target org', async () => {
62
+ mocked(file_helpers_1.loadData).mockReturnValue([]);
63
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
64
+ { id: '1', featureName: 'SHOWROOM' },
65
+ { id: '2', featureName: 'DEV_TABS' },
66
+ ]);
67
+ const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
68
+ await (0, import_1.uploadFeatureFlags)('export-dir');
69
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
70
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
71
+ expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('lists no feature flags'));
72
+ infoSpy.mockRestore();
73
+ });
74
+ it('treats a blank feature-flags.yml the same as an empty list', async () => {
75
+ mocked(file_helpers_1.loadData).mockReturnValue(undefined);
76
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
77
+ await (0, import_1.uploadFeatureFlags)('export-dir');
78
+ expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
79
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
80
+ });
81
+ it('enables every flag from the export that the target org is missing', async () => {
82
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }, { featureName: 'PROMOTE' }]);
83
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
84
+ await (0, import_1.uploadFeatureFlags)('export-dir');
85
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledTimes(1);
86
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledWith('PROMOTE');
87
+ });
88
+ it('disables flags the target org has that the export does not, so the orgs match', async () => {
89
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
90
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
91
+ { id: '1', featureName: 'SHOWROOM' },
92
+ { id: '2', featureName: 'DEV_TABS' },
93
+ ]);
94
+ await (0, import_1.uploadFeatureFlags)('export-dir');
95
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
96
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).toHaveBeenCalledTimes(1);
97
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).toHaveBeenCalledWith('2');
98
+ });
99
+ it('summarizes what it removed, and why, at the end of the run', async () => {
100
+ const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
101
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
102
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
103
+ { id: '1', featureName: 'SHOWROOM' },
104
+ { id: '2', featureName: 'PROMOTE' },
105
+ { id: '3', featureName: 'DEV_TABS' },
106
+ ]);
107
+ await (0, import_1.uploadFeatureFlags)('export-dir');
108
+ const summary = infoSpy.mock.calls.map(([message]) => String(message)).at(-1);
109
+ expect(summary).toContain('2 flag(s) were enabled in this org but not in the source');
110
+ expect(summary).toContain('DEV_TABS, PROMOTE');
111
+ infoSpy.mockRestore();
112
+ });
113
+ it('does not print a removal summary when nothing was removed', async () => {
114
+ const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
115
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
116
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
117
+ await (0, import_1.uploadFeatureFlags)('export-dir');
118
+ const messages = infoSpy.mock.calls.map(([message]) => String(message));
119
+ expect(messages.some((message) => message.includes('so they were removed'))).toBe(false);
120
+ infoSpy.mockRestore();
121
+ });
122
+ it('accepts plain feature names and ignores entries with no featureName', async () => {
123
+ mocked(file_helpers_1.loadData).mockReturnValue(['SHOWROOM', { featureName: ' PROMOTE ' }, {}, null]);
124
+ await (0, import_1.uploadFeatureFlags)('export-dir');
125
+ expect(mocked(feature_flags_1.createFeatureFlag).mock.calls.map(([name]) => name)).toEqual([
126
+ 'SHOWROOM',
127
+ 'PROMOTE',
128
+ ]);
129
+ });
130
+ it('rejects when the file is not a list', async () => {
131
+ mocked(file_helpers_1.loadData).mockReturnValue({ featureName: 'SHOWROOM' });
132
+ await expect((0, import_1.uploadFeatureFlags)('export-dir')).rejects.toThrow('must contain a YAML list of feature flags');
133
+ });
134
+ it('applies what it can and then rejects, rather than reporting success', async () => {
135
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }, { featureName: 'PROMOTE' }]);
136
+ mocked(feature_flags_1.createFeatureFlag).mockImplementation(async (featureName) => {
137
+ if (featureName === 'SHOWROOM') {
138
+ throw new Error('Forbidden resource');
139
+ }
140
+ return {};
141
+ });
142
+ await expect((0, import_1.uploadFeatureFlags)('export-dir')).rejects.toThrow('could not enable SHOWROOM: Forbidden resource');
143
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledWith('PROMOTE');
144
+ });
145
+ });
146
+ describe('uploadFeatureFlagsIfPermitted', () => {
147
+ let existsSyncSpy;
148
+ let infoSpy;
149
+ beforeEach(() => {
150
+ jest.clearAllMocks();
151
+ existsSyncSpy = jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(true);
152
+ infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
153
+ mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }]);
154
+ mocked(feature_flags_1.createFeatureFlag).mockResolvedValue({});
155
+ mocked(feature_flags_1.deleteFeatureFlag).mockResolvedValue({});
156
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([]);
157
+ });
158
+ afterEach(() => {
159
+ existsSyncSpy.mockRestore();
160
+ infoSpy.mockRestore();
161
+ });
162
+ const messages = () => infoSpy.mock.calls.map(([message]) => String(message)).join('\n');
163
+ it('applies the flags when the user has a platform role', async () => {
164
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({
165
+ platformRoles: ['PLATFORM_MANAGER'],
166
+ });
167
+ await (0, import_1.uploadFeatureFlagsIfPermitted)('export-dir');
168
+ expect(mocked(feature_flags_1.createFeatureFlag)).toHaveBeenCalledWith('SHOWROOM');
169
+ });
170
+ it('skips with a log instead of failing when the user has no platform role', async () => {
171
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({ platformRoles: [] });
172
+ await expect((0, import_1.uploadFeatureFlagsIfPermitted)('export-dir')).resolves.toBeUndefined();
173
+ expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
174
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
175
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
176
+ expect(messages()).toContain('Skipping feature flags');
177
+ expect(messages()).toContain('PLATFORM_ADMIN or PLATFORM_MANAGER');
178
+ expect(messages()).toContain('contrail feature-flags import -d export-dir');
179
+ });
180
+ it('skips with a log when the platform roles cannot be read', async () => {
181
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockRejectedValue(new Error('Forbidden resource'));
182
+ await expect((0, import_1.uploadFeatureFlagsIfPermitted)('export-dir')).resolves.toBeUndefined();
183
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
184
+ expect(messages()).toContain('Skipping feature flags');
185
+ expect(messages()).toContain('Forbidden resource');
186
+ });
187
+ it('still propagates a failure from the upload itself', async () => {
188
+ mocked(users_1.getCurrentUserWithPlatformRoles).mockResolvedValue({
189
+ platformRoles: ['PLATFORM_ADMIN'],
190
+ });
191
+ mocked(feature_flags_1.createFeatureFlag).mockRejectedValue(new Error('boom'));
192
+ await expect((0, import_1.uploadFeatureFlagsIfPermitted)('export-dir')).rejects.toThrow('could not enable SHOWROOM: boom');
193
+ });
194
+ });
@@ -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
  });
@@ -10,3 +10,12 @@ export default class ExportInstance extends BaseCommand {
10
10
  };
11
11
  run(): Promise<void>;
12
12
  }
13
+ /**
14
+ * Exports everything `contrail instance import` loads back: types, users, groups and the
15
+ * org's enabled feature flags. Org config (branding, sorting, admin-console settings) is
16
+ * written by getAllTypes, so it is not repeated here.
17
+ *
18
+ * Exporting feature flags is a plain read, so it works for any user — only the import side
19
+ * needs a platform role.
20
+ */
21
+ export declare function exportInstance(outDir: string, overwrite: boolean): Promise<void>;
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportInstance = 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");
6
7
  const getAll_1 = require("../types/getAll");
7
8
  const export_1 = require("../users/export");
8
9
  const export_2 = require("../groups/export");
10
+ const export_3 = require("../feature-flags/export");
9
11
  const basecommand_1 = require("../../basecommand");
10
12
  class ExportInstance extends basecommand_1.BaseCommand {
11
13
  static description = 'Exports an data of an instance including types and users';
@@ -29,9 +31,22 @@ class ExportInstance extends basecommand_1.BaseCommand {
29
31
  }),
30
32
  };
31
33
  async run() {
32
- await (0, getAll_1.getAll)(this.flags.outDir, this.flags.overwrite);
33
- await (0, export_1.exportUsers)(path.join(this.flags.outDir, 'users.yml'), this.flags.overwrite);
34
- await (0, export_2.exportGroups)(path.join(this.flags.outDir, 'groups.yml'), this.flags.overwrite);
34
+ await exportInstance(this.flags.outDir, this.flags.overwrite);
35
35
  }
36
36
  }
37
37
  exports.default = ExportInstance;
38
+ /**
39
+ * Exports everything `contrail instance import` loads back: types, users, groups and the
40
+ * org's enabled feature flags. Org config (branding, sorting, admin-console settings) is
41
+ * written by getAllTypes, so it is not repeated here.
42
+ *
43
+ * Exporting feature flags is a plain read, so it works for any user — only the import side
44
+ * needs a platform role.
45
+ */
46
+ async function exportInstance(outDir, overwrite) {
47
+ await (0, getAll_1.getAll)(outDir, overwrite);
48
+ await (0, export_1.exportUsers)(path.join(outDir, 'users.yml'), overwrite);
49
+ await (0, export_2.exportGroups)(path.join(outDir, 'groups.yml'), overwrite);
50
+ await (0, export_3.exportFeatureFlags)(outDir, overwrite);
51
+ }
52
+ exports.exportInstance = exportInstance;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const export_1 = require("./export");
4
+ const getAll_1 = require("../types/getAll");
5
+ const export_2 = require("../users/export");
6
+ const export_3 = require("../groups/export");
7
+ const export_4 = require("../feature-flags/export");
8
+ jest.mock('../types/getAll');
9
+ jest.mock('../users/export');
10
+ jest.mock('../groups/export');
11
+ jest.mock('../feature-flags/export');
12
+ const mocked = (fn) => fn;
13
+ const STEPS = [getAll_1.getAll, export_2.exportUsers, export_3.exportGroups, export_4.exportFeatureFlags];
14
+ describe('exportInstance', () => {
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ for (const fn of STEPS) {
18
+ mocked(fn).mockResolvedValue(undefined);
19
+ }
20
+ });
21
+ it('exports everything the import loads back, including feature flags', async () => {
22
+ await (0, export_1.exportInstance)('out-dir', true);
23
+ expect(mocked(getAll_1.getAll)).toHaveBeenCalledWith('out-dir', true);
24
+ expect(mocked(export_2.exportUsers)).toHaveBeenCalledWith('out-dir/users.yml', true);
25
+ expect(mocked(export_3.exportGroups)).toHaveBeenCalledWith('out-dir/groups.yml', true);
26
+ expect(mocked(export_4.exportFeatureFlags)).toHaveBeenCalledWith('out-dir', true);
27
+ });
28
+ it('passes the overwrite flag through to the feature flags export', async () => {
29
+ await (0, export_1.exportInstance)('out-dir', false);
30
+ expect(mocked(export_4.exportFeatureFlags)).toHaveBeenCalledWith('out-dir', false);
31
+ });
32
+ });
@@ -9,3 +9,20 @@ 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
+ * 6. feature flags — skipped with a log message when the user has no platform role,
21
+ * since applying them needs PLATFORM_ADMIN/PLATFORM_MANAGER but the rest of the
22
+ * import does not, and org admins are expected to run this command
23
+ *
24
+ * Fails (rejects) if the type overrides could not be applied, so the import does not
25
+ * report success while the org is missing customizations carried by overrides
26
+ * (labels/formulas/editability of core properties, subtype-level property changes).
27
+ */
28
+ 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,9 @@ 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");
14
+ const import_4 = require("../feature-flags/import");
11
15
  const basecommand_1 = require("../../basecommand");
12
16
  class ImportInstance extends basecommand_1.BaseCommand {
13
17
  static description = 'Import the data to an instance, including types and users';
@@ -25,11 +29,34 @@ class ImportInstance extends basecommand_1.BaseCommand {
25
29
  }),
26
30
  };
27
31
  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);
32
+ await importInstance(this.flags.dir);
33
33
  }
34
34
  }
35
35
  exports.default = ImportInstance;
36
+ /**
37
+ * Imports an instance export in dependency order:
38
+ * 1. option sets, types, option set hierarchies, rule sets
39
+ * 2. users and groups (policy principals)
40
+ * 3. type property policies and type policies
41
+ * 4. type overrides — after policies, because they reference policies, rule sets and option
42
+ * set hierarchies by slug and the API rejects the whole upload if any of them is missing
43
+ * 5. org config
44
+ * 6. feature flags — skipped with a log message when the user has no platform role,
45
+ * since applying them needs PLATFORM_ADMIN/PLATFORM_MANAGER but the rest of the
46
+ * import does not, and org admins are expected to run this command
47
+ *
48
+ * Fails (rejects) if the type overrides could not be applied, so the import does not
49
+ * report success while the org is missing customizations carried by overrides
50
+ * (labels/formulas/editability of core properties, subtype-level property changes).
51
+ */
52
+ async function importInstance(dir) {
53
+ await (0, loadAll_1.loadAll)(dir, true, false);
54
+ await (0, import_1.uploadUsers)(path.join(dir, 'users.yml'));
55
+ await (0, import_2.uploadGroups)(path.join(dir, 'groups.yml'));
56
+ await (0, uploadTypePropertyPolicies_1.uploadTypePropertyPolicies)(dir);
57
+ await (0, uploadTypePolicies_1.uploadTypePolicies)(dir);
58
+ await (0, uploadTypeOverrides_1.uploadTypeOverrides)(dir);
59
+ await (0, import_3.uploadOrgConfig)(dir);
60
+ await (0, import_4.uploadFeatureFlagsIfPermitted)(dir);
61
+ }
62
+ exports.importInstance = importInstance;
@@ -0,0 +1 @@
1
+ export {};