@jupiterone/integration-sdk-cli 8.24.0 → 8.24.1

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.
@@ -64,16 +64,10 @@ test('uploads data to the synchronization api and displays the results', async (
64
64
  });
65
65
  });
66
66
 
67
- test('hits dev urls if JUPITERONE_DEV environment variable is set', async () => {
68
- process.env.JUPITERONE_DEV = 'true';
69
-
67
+ test('skips finalization with skip-finalize', async () => {
70
68
  const job = generateSynchronizationJob();
71
69
 
72
- setupSynchronizerApi({
73
- polly,
74
- job,
75
- baseUrl: 'https://api.dev.jupiterone.io',
76
- });
70
+ setupSynchronizerApi({ polly, job, baseUrl: 'https://api.us.jupiterone.io' });
77
71
 
78
72
  await createCli().parseAsync([
79
73
  'node',
@@ -81,12 +75,13 @@ test('hits dev urls if JUPITERONE_DEV environment variable is set', async () =>
81
75
  'sync',
82
76
  '--integrationInstanceId',
83
77
  'test',
78
+ '--skip-finalize',
84
79
  ]);
85
80
 
86
81
  expect(log.displaySynchronizationResults).toHaveBeenCalledTimes(1);
87
82
  expect(log.displaySynchronizationResults).toHaveBeenCalledWith({
88
83
  ...job,
89
- status: SynchronizationJobStatus.FINALIZE_PENDING,
84
+ status: SynchronizationJobStatus.AWAITING_UPLOADS,
90
85
  // We arrive at these numbers because of what
91
86
  // was written to disk in the 'synchronization' project fixture
92
87
  numEntitiesUploaded: 6,
@@ -123,72 +118,3 @@ test('does not publish events for source "api" since there is no integrationJobI
123
118
  expect(eventsPublished).toBe(false);
124
119
  expect(log.displaySynchronizationResults).toHaveBeenCalledTimes(1);
125
120
  });
126
-
127
- test('hits different url if --api-base-url is set', async () => {
128
- const job = generateSynchronizationJob();
129
-
130
- setupSynchronizerApi({
131
- polly,
132
- job,
133
- baseUrl: 'https://api.TEST.jupiterone.io',
134
- });
135
-
136
- await createCli().parseAsync([
137
- 'node',
138
- 'j1-integration',
139
- 'sync',
140
- '--integrationInstanceId',
141
- 'test',
142
- '--api-base-url',
143
- 'https://api.TEST.jupiterone.io',
144
- ]);
145
-
146
- expect(log.displaySynchronizationResults).toHaveBeenCalledTimes(1);
147
- expect(log.displaySynchronizationResults).toHaveBeenCalledWith({
148
- ...job,
149
- status: SynchronizationJobStatus.FINALIZE_PENDING,
150
- // We arrive at these numbers because of what
151
- // was written to disk in the 'synchronization' project fixture
152
- numEntitiesUploaded: 6,
153
- numRelationshipsUploaded: 3,
154
- });
155
- });
156
-
157
- test('throws an error if --api-base-url is set with --development', async () => {
158
- const job = generateSynchronizationJob();
159
-
160
- setupSynchronizerApi({
161
- polly,
162
- job,
163
- baseUrl: 'https://api.TEST.jupiterone.io',
164
- });
165
-
166
- await expect(
167
- createCli().parseAsync([
168
- 'node',
169
- 'j1-integration',
170
- 'sync',
171
- '--integrationInstanceId',
172
- 'test',
173
- '--development',
174
- '--api-base-url',
175
- 'https://api.TEST.jupiterone.io',
176
- ]),
177
- ).rejects.toThrow(
178
- 'Invalid configuration supplied. Cannot specify both --api-base-url and --development(-d) flags.',
179
- );
180
- });
181
-
182
- test('throws if JUPITERONE_API_KEY is not set', async () => {
183
- delete process.env.JUPITERONE_API_KEY;
184
-
185
- await expect(
186
- createCli().parseAsync([
187
- 'node',
188
- 'j1-integration',
189
- 'sync',
190
- '--integrationInstanceId',
191
- 'test',
192
- ]),
193
- ).rejects.toThrow('JUPITERONE_API_KEY environment variable must be set');
194
- });
@@ -0,0 +1,223 @@
1
+ import { createCommand } from 'commander';
2
+ import {
3
+ addApiClientOptionsToCommand,
4
+ addSyncOptionsToCommand,
5
+ ApiClientOptions,
6
+ getApiClientOptions,
7
+ SyncOptions,
8
+ validateApiClientOptions,
9
+ validateSyncOptions,
10
+ } from '../commands/options';
11
+ import {
12
+ DEFAULT_UPLOAD_BATCH_SIZE,
13
+ JUPITERONE_DEV_API_BASE_URL,
14
+ JUPITERONE_PROD_API_BASE_URL,
15
+ } from '@jupiterone/integration-sdk-runtime';
16
+
17
+ const syncOptions = (values?: Partial<SyncOptions>): SyncOptions => ({
18
+ source: 'integration-managed',
19
+ uploadBatchSize: DEFAULT_UPLOAD_BATCH_SIZE,
20
+ uploadRelationshipBatchSize: DEFAULT_UPLOAD_BATCH_SIZE,
21
+ skipFinalize: false,
22
+ ...values,
23
+ });
24
+
25
+ const apiClientOptions = (
26
+ values?: Partial<ApiClientOptions>,
27
+ ): ApiClientOptions => ({
28
+ apiBaseUrl: JUPITERONE_PROD_API_BASE_URL,
29
+ development: false,
30
+ ...values,
31
+ });
32
+
33
+ describe('addSyncOptionsToCommand', () => {
34
+ test('default values', () => {
35
+ expect(
36
+ addSyncOptionsToCommand(createCommand())
37
+ .parse(['node', 'command'])
38
+ .opts<SyncOptions>(),
39
+ ).toEqual(syncOptions());
40
+ });
41
+
42
+ test('--integrationInstanceId', () => {
43
+ expect(
44
+ addSyncOptionsToCommand(createCommand())
45
+ .parse(['node', 'command', '--integrationInstanceId', 'test'])
46
+ .opts(),
47
+ ).toEqual(syncOptions({ integrationInstanceId: 'test' }));
48
+ });
49
+
50
+ test('--source', () => {
51
+ expect(
52
+ addSyncOptionsToCommand(createCommand())
53
+ .parse(['node', 'command', '--source', 'api'])
54
+ .opts(),
55
+ ).toEqual(syncOptions({ source: 'api' }));
56
+ });
57
+
58
+ test('--source unknown', () => {
59
+ expect(() => {
60
+ addSyncOptionsToCommand(createCommand())
61
+ .parse(['node', 'command', '--source', 'unknown'])
62
+ .opts();
63
+ }).toThrowError(/must be/);
64
+ });
65
+ });
66
+
67
+ describe('validateSyncOptions', () => {
68
+ test('valid', () => {
69
+ expect(() =>
70
+ validateSyncOptions(syncOptions({ integrationInstanceId: 'test' })),
71
+ ).not.toThrow();
72
+ });
73
+
74
+ test('source: api and integrationInstanceId', () => {
75
+ expect(() =>
76
+ validateSyncOptions(
77
+ syncOptions({ integrationInstanceId: 'test', source: 'api' }),
78
+ ),
79
+ ).toThrowError(/both --source api and --integrationInstanceId/);
80
+ });
81
+
82
+ test('source: api without scope', () => {
83
+ expect(() =>
84
+ validateSyncOptions(syncOptions({ source: 'api' })),
85
+ ).toThrowError(/--source api requires --scope/);
86
+ });
87
+
88
+ test('defaults with no integrationInstanceId', () => {
89
+ expect(() => validateSyncOptions(syncOptions())).toThrowError(
90
+ /--integrationInstanceId or --source api/,
91
+ );
92
+ });
93
+ });
94
+
95
+ describe('addApiClientOptionsToCommand', () => {
96
+ test('default values', () => {
97
+ expect(
98
+ addApiClientOptionsToCommand(createCommand())
99
+ .parse(['node', 'command'])
100
+ .opts<SyncOptions>(),
101
+ ).toEqual(apiClientOptions());
102
+ });
103
+
104
+ test('--development with default --api-base-url', () => {
105
+ expect(
106
+ addApiClientOptionsToCommand(createCommand())
107
+ .parse(['node', 'command', '--development'])
108
+ .opts(),
109
+ ).toEqual(
110
+ apiClientOptions({
111
+ development: true,
112
+ apiBaseUrl: JUPITERONE_PROD_API_BASE_URL,
113
+ }),
114
+ );
115
+ });
116
+ });
117
+
118
+ describe('validApiClientOptions', () => {
119
+ test('valid', () => {
120
+ expect(() => validateApiClientOptions(apiClientOptions())).not.toThrow();
121
+ });
122
+
123
+ test('--development with default --api-base-url', () => {
124
+ expect(() =>
125
+ validateApiClientOptions(
126
+ apiClientOptions({
127
+ development: true,
128
+ apiBaseUrl: JUPITERONE_PROD_API_BASE_URL,
129
+ }),
130
+ ),
131
+ ).not.toThrow();
132
+ });
133
+
134
+ test('--development with --api-base-url JUPITERONE_DEV_API_BASE_URL', () => {
135
+ expect(() =>
136
+ validateApiClientOptions(
137
+ apiClientOptions({
138
+ development: true,
139
+ apiBaseUrl: JUPITERONE_DEV_API_BASE_URL,
140
+ }),
141
+ ),
142
+ ).not.toThrow();
143
+ });
144
+
145
+ test('--development with --api-base-url', () => {
146
+ expect(() =>
147
+ validateApiClientOptions(
148
+ apiClientOptions({
149
+ development: true,
150
+ apiBaseUrl: 'https://example.com',
151
+ }),
152
+ ),
153
+ ).toThrow(/both --development and --api-base-url/);
154
+ });
155
+ });
156
+
157
+ describe('getApiClientOptions', () => {
158
+ beforeEach(() => {
159
+ process.env.JUPITERONE_ACCOUNT = 'test account';
160
+ process.env.JUPITERONE_API_KEY = 'test api key';
161
+ });
162
+
163
+ afterEach(() => {
164
+ delete process.env.JUPITERONE_ACCOUNT;
165
+ delete process.env.JUPITERONE_API_KEY;
166
+ });
167
+
168
+ test('defaults with valid environment', () => {
169
+ expect(getApiClientOptions(apiClientOptions())).toEqual({
170
+ apiBaseUrl: JUPITERONE_PROD_API_BASE_URL,
171
+ accessToken: 'test api key',
172
+ account: 'test account',
173
+ });
174
+ });
175
+
176
+ test('defaults with no JUPITERONE_API_KEY', () => {
177
+ delete process.env.JUPITERONE_API_KEY;
178
+ expect(() => getApiClientOptions(apiClientOptions())).toThrowError(
179
+ /JUPITERONE_API_KEY/,
180
+ );
181
+ });
182
+
183
+ test('defaults with no JUPITERONE_ACCOUNT', () => {
184
+ delete process.env.JUPITERONE_ACCOUNT;
185
+ expect(() => getApiClientOptions(apiClientOptions())).toThrowError(
186
+ /JUPITERONE_ACCOUNT/,
187
+ );
188
+ });
189
+
190
+ test('--development', () => {
191
+ expect(
192
+ getApiClientOptions(apiClientOptions({ development: true })),
193
+ ).toEqual({
194
+ apiBaseUrl: JUPITERONE_DEV_API_BASE_URL,
195
+ accessToken: 'test api key',
196
+ account: 'test account',
197
+ });
198
+ });
199
+
200
+ test(`--api-base-url ${JUPITERONE_DEV_API_BASE_URL}`, () => {
201
+ expect(
202
+ getApiClientOptions(
203
+ apiClientOptions({ apiBaseUrl: JUPITERONE_DEV_API_BASE_URL }),
204
+ ),
205
+ ).toEqual({
206
+ apiBaseUrl: JUPITERONE_DEV_API_BASE_URL,
207
+ accessToken: 'test api key',
208
+ account: 'test account',
209
+ });
210
+ });
211
+
212
+ test(`--api-base-url ${JUPITERONE_PROD_API_BASE_URL}`, () => {
213
+ expect(
214
+ getApiClientOptions(
215
+ apiClientOptions({ apiBaseUrl: JUPITERONE_PROD_API_BASE_URL }),
216
+ ),
217
+ ).toEqual({
218
+ apiBaseUrl: JUPITERONE_PROD_API_BASE_URL,
219
+ accessToken: 'test api key',
220
+ account: 'test account',
221
+ });
222
+ });
223
+ });
@@ -11,6 +11,7 @@ import {
11
11
 
12
12
  import { loadConfig } from '../config';
13
13
  import * as log from '../log';
14
+ import { addPathOptionsToCommand, configureRuntimeFilesystem } from './options';
14
15
 
15
16
  // coercion function to collect multiple values for a flag
16
17
  const collector = (value: string, arr: string[]) => {
@@ -19,13 +20,11 @@ const collector = (value: string, arr: string[]) => {
19
20
  };
20
21
 
21
22
  export function collect() {
22
- return createCommand('collect')
23
+ const command = createCommand('collect');
24
+ addPathOptionsToCommand(command);
25
+
26
+ return command
23
27
  .description('collect data and store entities and relationships to disk')
24
- .option(
25
- '-p, --project-path <directory>',
26
- 'path to integration project directory',
27
- process.cwd(),
28
- )
29
28
  .option(
30
29
  '-s, --step <steps>',
31
30
  'step(s) to run, comma separated. Utilizes available caches to speed up dependent steps.',
@@ -42,6 +41,8 @@ export function collect() {
42
41
  )
43
42
  .option('-V, --disable-schema-validation', 'disable schema validation')
44
43
  .action(async (options) => {
44
+ configureRuntimeFilesystem(options);
45
+
45
46
  if (!options.cache && options.step.length === 0) {
46
47
  throw new Error(
47
48
  'Invalid option: Option --no-cache requires option --step to also be specified.',
@@ -53,13 +54,6 @@ export function collect() {
53
54
  );
54
55
  }
55
56
 
56
- // Point `fileSystem.ts` functions to expected location relative to
57
- // integration project path.
58
- process.env.JUPITERONE_INTEGRATION_STORAGE_DIRECTORY = path.resolve(
59
- options.projectPath,
60
- '.j1-integration',
61
- );
62
-
63
57
  if (options.step.length > 0 && options.cache && !options.cachePath) {
64
58
  // Step option was used, cache is wanted, and no cache path was provided
65
59
  // therefore, copy .j1-integration into .j1-integration-cache
@@ -146,6 +146,7 @@ function diffEntities(
146
146
  };
147
147
  }
148
148
 
149
+ /* eslint-disable no-console */
149
150
  console.log('--- ENTITY DIFF ---');
150
151
  console.log(diffString(oldEntities, newEntities, undefined, { keysOnly }));
151
152
  }
@@ -12,6 +12,7 @@ import { IntegrationInvocationConfigLoadError, loadConfig } from '../config';
12
12
  import { promises as fs } from 'fs';
13
13
  import * as log from '../log';
14
14
 
15
+ /* eslint-disable no-console */
15
16
  export function generateIntegrationGraphSchemaCommand() {
16
17
  return createCommand('generate-integration-graph-schema')
17
18
  .description(
@@ -1,51 +1,97 @@
1
- import { getApiBaseUrl } from '@jupiterone/integration-sdk-runtime';
1
+ import {
2
+ DEFAULT_UPLOAD_BATCH_SIZE,
3
+ getAccountFromEnvironment,
4
+ getApiKeyFromEnvironment,
5
+ JUPITERONE_DEV_API_BASE_URL,
6
+ JUPITERONE_PROD_API_BASE_URL,
7
+ } from '@jupiterone/integration-sdk-runtime';
8
+ import { Command, OptionValues } from 'commander';
9
+ import path from 'path';
10
+
11
+ export interface PathOptions {
12
+ projectPath: string;
13
+ }
14
+
15
+ export function addPathOptionsToCommand(command: Command): Command {
16
+ return command.option(
17
+ '-p, --project-path <directory>',
18
+ 'path to integration project directory',
19
+ process.cwd(),
20
+ );
21
+ }
2
22
 
3
23
  /**
4
- * Determines the JupiterOne `apiBaseUrl` based on these mutually-exclusive `options`:
5
- * - --api-base-url - a specific URL to use
6
- * - --development - use the development API
7
- *
8
- * @throws Error if both --api-base-url and --development are specified
24
+ * Configure the filesystem interaction code to function against `${options.projectPath}/.j1-integration by setting the
25
+ * global `process.env.JUPITERONE_INTEGRATION_STORAGE_DIRECTORY` variable.
9
26
  */
10
- export function getApiBaseUrlOption(options: any): string {
11
- let apiBaseUrl: string;
12
- if (options.apiBaseUrl) {
13
- if (options.development) {
14
- throw new Error(
15
- 'Invalid configuration supplied. Cannot specify both --api-base-url and --development(-d) flags.',
16
- );
17
- }
18
- apiBaseUrl = options.apiBaseUrl;
19
- } else {
20
- apiBaseUrl = getApiBaseUrl({
21
- dev: options.development,
22
- });
23
- }
24
- return apiBaseUrl;
27
+ export function configureRuntimeFilesystem(options: PathOptions): void {
28
+ process.env.JUPITERONE_INTEGRATION_STORAGE_DIRECTORY = path.resolve(
29
+ options.projectPath,
30
+ '.j1-integration',
31
+ );
25
32
  }
26
33
 
27
- interface SynchronizationJobSourceOptions {
34
+ export interface SyncOptions {
28
35
  source: 'integration-managed' | 'integration-external' | 'api';
29
- scope?: string;
30
- integrationInstanceId?: string;
36
+ scope?: string | undefined;
37
+ integrationInstanceId?: string | undefined;
38
+ uploadBatchSize: number;
39
+ uploadRelationshipBatchSize: number;
40
+ skipFinalize: boolean;
41
+ }
42
+
43
+ export function addSyncOptionsToCommand(command: Command): Command {
44
+ return command
45
+ .option(
46
+ '-i, --integrationInstanceId <id>',
47
+ '_integrationInstanceId assigned to uploaded entities and relationships',
48
+ )
49
+ .option(
50
+ '--source <integration-managed|integration-external|api>',
51
+ 'specify synchronization job source value',
52
+ (value: string) => {
53
+ if (
54
+ value !== 'integration-managed' &&
55
+ value !== 'integration-external' &&
56
+ value !== 'api'
57
+ ) {
58
+ throw new Error(
59
+ `--source must be one of "integration-managed", "integration-external", or "api"`,
60
+ );
61
+ } else {
62
+ return value;
63
+ }
64
+ },
65
+ 'integration-managed',
66
+ )
67
+ .option('--scope <anystring>', 'specify synchronization job scope value')
68
+ .option(
69
+ '-u, --upload-batch-size <number>',
70
+ 'specify number of entities and relationships per upload batch',
71
+ (value, _previous: Number) => Number(value),
72
+ DEFAULT_UPLOAD_BATCH_SIZE,
73
+ )
74
+ .option(
75
+ '-ur, --upload-relationship-batch-size <number>',
76
+ 'specify number of relationships per upload batch, overrides --upload-batch-size',
77
+ (value, _previous: Number) => Number(value),
78
+ DEFAULT_UPLOAD_BATCH_SIZE,
79
+ )
80
+ .option(
81
+ '--skip-finalize',
82
+ 'skip synchronization finalization to leave job open for additional uploads',
83
+ false,
84
+ );
31
85
  }
32
86
 
33
87
  /**
34
- * Determines the `source` configuration for the synchronization job based on these `options`:
35
- * - --source - a specific source to use
36
- * - --scope - a specific scope to use, required when --source is "api"
37
- * - --integrationInstanceId - a specific integrationInstanceId to use, required when --source is not "api"
88
+ * Validates options for the synchronization job.
38
89
  *
39
- * @throws Error if --source is "api" and --scope is not specified
40
- * @throws Error if --source is "api" and --integrationInstanceId is specified
41
- * @throws Error if one of --integrationInstanceId or --source is not specified
90
+ * @throws {Error} if --source is "api" and --scope is not specified
91
+ * @throws {Error} if --source is "api" and --integrationInstanceId is specified
92
+ * @throws {Error} if one of --integrationInstanceId or --source is not specified
42
93
  */
43
- export function getSynchronizationJobSourceOptions(
44
- options: any,
45
- ): SynchronizationJobSourceOptions {
46
- const synchronizeOptions: SynchronizationJobSourceOptions = {
47
- source: options.source,
48
- };
94
+ export function validateSyncOptions(options: SyncOptions): SyncOptions {
49
95
  if (options.source === 'api') {
50
96
  if (options.integrationInstanceId)
51
97
  throw new Error(
@@ -55,19 +101,90 @@ export function getSynchronizationJobSourceOptions(
55
101
  throw new Error(
56
102
  'Invalid configuration supplied. --source api requires --scope flag.',
57
103
  );
58
- synchronizeOptions.scope = options.scope;
59
104
  } else if (
60
105
  !['integration-managed', 'integration-external'].includes(options.source)
61
106
  ) {
62
107
  throw new Error(
63
108
  `Invalid configuration supplied. --source must be one of: integration-managed, integration-external, api. Received: ${options.source}.`,
64
109
  );
65
- } else if (options.integrationInstanceId) {
66
- synchronizeOptions.integrationInstanceId = options.integrationInstanceId;
67
- } else {
110
+ } else if (!options.integrationInstanceId) {
68
111
  throw new Error(
69
112
  'Invalid configuration supplied. --integrationInstanceId or --source api and --scope required.',
70
113
  );
71
114
  }
72
- return synchronizeOptions;
115
+ return options;
116
+ }
117
+
118
+ /**
119
+ * Returns an object containing only `SyncOptions` properties. This helps to ensure other properties of `OptionValues` are not passed to the `SynchronizationJob`.
120
+ */
121
+ export function getSyncOptions(options: OptionValues): SyncOptions {
122
+ return {
123
+ source: options.source,
124
+ scope: options.scope,
125
+ integrationInstanceId: options.integrationInstanceId,
126
+ uploadBatchSize: options.uploadBatchSize,
127
+ uploadRelationshipBatchSize: options.uploadRelationshipBatchSize,
128
+ skipFinalize: options.skipFinalize,
129
+ };
130
+ }
131
+
132
+ export interface ApiClientOptions {
133
+ development: boolean;
134
+ apiBaseUrl: string;
135
+ apiKey?: string;
136
+ account?: string;
137
+ }
138
+
139
+ export function addApiClientOptionsToCommand(command: Command): Command {
140
+ return command
141
+ .option(
142
+ '--api-base-url <url>',
143
+ 'specify synchronization API base URL',
144
+ JUPITERONE_PROD_API_BASE_URL,
145
+ )
146
+ .option(
147
+ '-d, --development',
148
+ '"true" to target apps.dev.jupiterone.io (JUPITERONE_DEV environment variable)',
149
+ !!process.env.JUPITERONE_DEV,
150
+ )
151
+ .option(
152
+ '--account <account>',
153
+ 'JupiterOne account ID (JUPITERONE_ACCOUNT environment variable)',
154
+ process.env.JUPITERONE_ACCOUNT,
155
+ )
156
+ .option(
157
+ '--api-key <key>',
158
+ 'JupiterOne API key (JUPITERONE_API_KEY environment variable)',
159
+ process.env.JUPITERONE_API_KEY?.replace(/./, '*'),
160
+ );
161
+ }
162
+
163
+ export function validateApiClientOptions(options: ApiClientOptions) {
164
+ if (
165
+ options.development &&
166
+ ![JUPITERONE_PROD_API_BASE_URL, JUPITERONE_DEV_API_BASE_URL].includes(
167
+ options.apiBaseUrl,
168
+ )
169
+ ) {
170
+ throw new Error(
171
+ `Invalid configuration supplied. Cannot specify both --development and --api-base-url flags.`,
172
+ );
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Builds a set of options for the 'createApiClient' function.
178
+ *
179
+ * @throws IntegrationApiKeyRequiredError
180
+ * @throws IntegrationAccountRequiredError
181
+ */
182
+ export function getApiClientOptions(options: ApiClientOptions) {
183
+ return {
184
+ apiBaseUrl: options.development
185
+ ? JUPITERONE_DEV_API_BASE_URL
186
+ : options.apiBaseUrl,
187
+ accessToken: options.apiKey || getApiKeyFromEnvironment(),
188
+ account: options.account || getAccountFromEnvironment(),
189
+ };
73
190
  }