@friggframework/devtools 2.0.0--canary.623.b39b42d.0 → 2.0.0--canary.625.950ba82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/__tests__/unit/commands/generate-iam.test.js +97 -0
- package/frigg-cli/deploy-command/index.js +44 -0
- package/frigg-cli/generate-command/index.js +2 -1
- package/frigg-cli/generate-iam-command.js +2 -1
- package/frigg-cli/index.js +16 -1
- package/frigg-cli/ssm-command/index.js +270 -0
- package/frigg-cli/ssm-command/index.test.js +301 -0
- package/infrastructure/__tests__/helpers/test-utils.js +3 -5
- package/infrastructure/__tests__/scoped-environment.test.js +126 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +17 -3
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +45 -0
- package/infrastructure/domains/database/migration-builder.js +46 -9
- package/infrastructure/domains/database/migration-builder.test.js +101 -0
- package/infrastructure/domains/integration/integration-builder.js +65 -9
- package/infrastructure/domains/integration/integration-builder.test.js +95 -0
- package/infrastructure/domains/networking/vpc-builder.js +57 -7
- package/infrastructure/domains/networking/vpc-builder.test.js +41 -0
- package/infrastructure/domains/networking/vpc-resolver.js +12 -3
- package/infrastructure/domains/networking/vpc-resolver.test.js +40 -0
- package/infrastructure/domains/parameters/offload-utils.js +175 -0
- package/infrastructure/domains/parameters/offload-utils.test.js +193 -0
- package/infrastructure/domains/parameters/ssm-builder.js +60 -14
- package/infrastructure/domains/parameters/ssm-builder.test.js +145 -1
- package/infrastructure/domains/scheduler/scheduler-builder.js +44 -8
- package/infrastructure/domains/scheduler/scheduler-builder.test.js +118 -0
- package/infrastructure/domains/security/iam-generator.js +33 -1
- package/infrastructure/domains/security/iam-generator.test.js +73 -0
- package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +15 -0
- package/infrastructure/domains/security/templates/iam-policy-full.json +7 -2
- package/infrastructure/domains/shared/builder-orchestrator.js +14 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +45 -0
- package/infrastructure/domains/shared/environment-builder.js +43 -7
- package/infrastructure/domains/shared/environment-builder.test.js +97 -1
- package/infrastructure/domains/shared/function-environments.js +97 -0
- package/infrastructure/domains/shared/function-environments.test.js +146 -0
- package/infrastructure/infrastructure-composer.js +5 -0
- package/infrastructure/infrastructure-composer.test.js +69 -14
- package/infrastructure/integration.test.js +3 -5
- package/package.json +8 -7
|
@@ -318,5 +318,106 @@ describe('MigrationBuilder', () => {
|
|
|
318
318
|
);
|
|
319
319
|
});
|
|
320
320
|
});
|
|
321
|
+
|
|
322
|
+
describe('scoped environment (lambda.scopedEnvironment)', () => {
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('scopes migration vars to the migration functions, keeping DB_TYPE global', async () => {
|
|
328
|
+
const result = await builder.build(
|
|
329
|
+
{ lambda: { scopedEnvironment: true } },
|
|
330
|
+
{}
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(result.environment.S3_BUCKET_NAME).toBeUndefined();
|
|
334
|
+
expect(result.environment.MIGRATION_STATUS_BUCKET).toBeUndefined();
|
|
335
|
+
expect(result.environment.DB_MIGRATION_QUEUE_URL).toBeUndefined();
|
|
336
|
+
expect(result.environment.DB_TYPE).toBe('postgresql');
|
|
337
|
+
|
|
338
|
+
for (const fnName of ['dbMigrationRouter', 'dbMigrationWorker']) {
|
|
339
|
+
expect(result.functionEnvironments[fnName]).toMatchObject({
|
|
340
|
+
S3_BUCKET_NAME: { Ref: 'FriggMigrationStatusBucket' },
|
|
341
|
+
MIGRATION_STATUS_BUCKET: {
|
|
342
|
+
Ref: 'FriggMigrationStatusBucket',
|
|
343
|
+
},
|
|
344
|
+
DB_MIGRATION_QUEUE_URL: { Ref: 'DbMigrationQueue' },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('broadcasts app-wide when the flag is off', async () => {
|
|
350
|
+
const result = await builder.build({}, {});
|
|
351
|
+
|
|
352
|
+
expect(result.environment.S3_BUCKET_NAME).toEqual({
|
|
353
|
+
Ref: 'FriggMigrationStatusBucket',
|
|
354
|
+
});
|
|
355
|
+
expect(result.environment.DB_MIGRATION_QUEUE_URL).toEqual({
|
|
356
|
+
Ref: 'DbMigrationQueue',
|
|
357
|
+
});
|
|
358
|
+
expect(result.functionEnvironments).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe('scoped environment (external migration resources)', () => {
|
|
363
|
+
// managementMode='managed' + vpcIsolation='shared' resolves both
|
|
364
|
+
// resources to EXTERNAL when discovered, driving the external path.
|
|
365
|
+
const externalDiscovery = {
|
|
366
|
+
migrationStatusBucket: 'external-migration-bucket',
|
|
367
|
+
migrationQueueUrl:
|
|
368
|
+
'https://sqs.us-east-1.amazonaws.com/123456789012/external-migration-queue',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
beforeEach(() => {
|
|
372
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('scopes external migration vars to the migration functions, keeping DB_TYPE global', async () => {
|
|
376
|
+
const result = await builder.build(
|
|
377
|
+
{
|
|
378
|
+
managementMode: 'managed',
|
|
379
|
+
vpcIsolation: 'shared',
|
|
380
|
+
lambda: { scopedEnvironment: true },
|
|
381
|
+
},
|
|
382
|
+
externalDiscovery
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expect(result.environment.S3_BUCKET_NAME).toBeUndefined();
|
|
386
|
+
expect(result.environment.MIGRATION_STATUS_BUCKET).toBeUndefined();
|
|
387
|
+
expect(result.environment.DB_MIGRATION_QUEUE_URL).toBeUndefined();
|
|
388
|
+
expect(result.environment.DB_TYPE).toBe('postgresql');
|
|
389
|
+
|
|
390
|
+
for (const fnName of ['dbMigrationRouter', 'dbMigrationWorker']) {
|
|
391
|
+
expect(result.functionEnvironments[fnName]).toMatchObject({
|
|
392
|
+
S3_BUCKET_NAME: externalDiscovery.migrationStatusBucket,
|
|
393
|
+
MIGRATION_STATUS_BUCKET:
|
|
394
|
+
externalDiscovery.migrationStatusBucket,
|
|
395
|
+
DB_MIGRATION_QUEUE_URL: externalDiscovery.migrationQueueUrl,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('broadcasts external migration vars app-wide when the flag is off', async () => {
|
|
401
|
+
const result = await builder.build(
|
|
402
|
+
{
|
|
403
|
+
managementMode: 'managed',
|
|
404
|
+
vpcIsolation: 'shared',
|
|
405
|
+
},
|
|
406
|
+
externalDiscovery
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(result.environment.S3_BUCKET_NAME).toBe(
|
|
410
|
+
externalDiscovery.migrationStatusBucket
|
|
411
|
+
);
|
|
412
|
+
expect(result.environment.MIGRATION_STATUS_BUCKET).toBe(
|
|
413
|
+
externalDiscovery.migrationStatusBucket
|
|
414
|
+
);
|
|
415
|
+
expect(result.environment.DB_MIGRATION_QUEUE_URL).toBe(
|
|
416
|
+
externalDiscovery.migrationQueueUrl
|
|
417
|
+
);
|
|
418
|
+
expect(result.environment.DB_TYPE).toBe('postgresql');
|
|
419
|
+
expect(result.functionEnvironments).toBeUndefined();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
321
422
|
});
|
|
322
423
|
|
|
@@ -24,6 +24,11 @@ const {
|
|
|
24
24
|
createEmptyDiscoveryResult,
|
|
25
25
|
ResourceOwnership,
|
|
26
26
|
} = require('../shared/types');
|
|
27
|
+
const {
|
|
28
|
+
isScopedEnvironmentActive,
|
|
29
|
+
getIntegrationFunctionNames,
|
|
30
|
+
getAdminFunctionNames,
|
|
31
|
+
} = require('../shared/function-environments');
|
|
27
32
|
|
|
28
33
|
class IntegrationBuilder extends InfrastructureBuilder {
|
|
29
34
|
constructor() {
|
|
@@ -218,13 +223,18 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
218
223
|
console.log(
|
|
219
224
|
` ✓ Creating ${integrationName}Queue in stack`
|
|
220
225
|
);
|
|
221
|
-
this.createIntegrationQueue(
|
|
226
|
+
this.createIntegrationQueue(
|
|
227
|
+
integrationName,
|
|
228
|
+
result,
|
|
229
|
+
appDefinition
|
|
230
|
+
);
|
|
222
231
|
} else {
|
|
223
232
|
console.log(` ✓ Using external ${integrationName}Queue`);
|
|
224
233
|
this.useExternalIntegrationQueue(
|
|
225
234
|
integrationName,
|
|
226
235
|
queueDecision,
|
|
227
|
-
result
|
|
236
|
+
result,
|
|
237
|
+
appDefinition
|
|
228
238
|
);
|
|
229
239
|
}
|
|
230
240
|
}
|
|
@@ -544,7 +554,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
544
554
|
/**
|
|
545
555
|
* Create integration-specific SQS queue CloudFormation resource
|
|
546
556
|
*/
|
|
547
|
-
createIntegrationQueue(integrationName, result) {
|
|
557
|
+
createIntegrationQueue(integrationName, result, appDefinition) {
|
|
548
558
|
const queueReference = `${this.capitalizeFirst(integrationName)}Queue`;
|
|
549
559
|
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
550
560
|
|
|
@@ -564,9 +574,12 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
564
574
|
};
|
|
565
575
|
|
|
566
576
|
// Add queue URL to environment
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
577
|
+
this.setQueueUrlEnvironment(
|
|
578
|
+
integrationName,
|
|
579
|
+
{ Ref: queueReference },
|
|
580
|
+
result,
|
|
581
|
+
appDefinition
|
|
582
|
+
);
|
|
570
583
|
|
|
571
584
|
// Add queue name to custom section
|
|
572
585
|
result.custom[queueReference] = queueName;
|
|
@@ -577,14 +590,57 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
577
590
|
/**
|
|
578
591
|
* Use external integration queue
|
|
579
592
|
*/
|
|
580
|
-
useExternalIntegrationQueue(
|
|
593
|
+
useExternalIntegrationQueue(
|
|
594
|
+
integrationName,
|
|
595
|
+
decision,
|
|
596
|
+
result,
|
|
597
|
+
appDefinition
|
|
598
|
+
) {
|
|
581
599
|
// Add queue URL to environment for Lambda functions
|
|
582
|
-
|
|
583
|
-
|
|
600
|
+
this.setQueueUrlEnvironment(
|
|
601
|
+
integrationName,
|
|
602
|
+
decision.physicalId,
|
|
603
|
+
result,
|
|
604
|
+
appDefinition
|
|
605
|
+
);
|
|
584
606
|
|
|
585
607
|
console.log(` ✓ Using external queue: ${decision.physicalId}`);
|
|
586
608
|
}
|
|
587
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Broadcast the queue URL app-wide, or — with lambda.scopedEnvironment —
|
|
612
|
+
* scope it to the functions that can actually enqueue: the shared auth
|
|
613
|
+
* router (dispatches integration actions synchronously), the admin-script
|
|
614
|
+
* functions (instantiate arbitrary integrations), and the owning
|
|
615
|
+
* integration's own functions.
|
|
616
|
+
*/
|
|
617
|
+
setQueueUrlEnvironment(integrationName, value, result, appDefinition) {
|
|
618
|
+
const key = `${integrationName.toUpperCase()}_QUEUE_URL`;
|
|
619
|
+
|
|
620
|
+
if (!isScopedEnvironmentActive(appDefinition)) {
|
|
621
|
+
result.environment[key] = value;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const integration = appDefinition.integrations.find(
|
|
626
|
+
(entry) => entry.Definition.name === integrationName
|
|
627
|
+
);
|
|
628
|
+
const targets = [
|
|
629
|
+
'auth',
|
|
630
|
+
...getAdminFunctionNames(appDefinition),
|
|
631
|
+
...getIntegrationFunctionNames(integration),
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
result.functionEnvironments = result.functionEnvironments || {};
|
|
635
|
+
for (const fnName of targets) {
|
|
636
|
+
result.functionEnvironments[fnName] = {
|
|
637
|
+
...result.functionEnvironments[fnName],
|
|
638
|
+
[key]: value,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
console.log(` ✓ Scoped ${key} to: ${targets.join(', ')}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
588
644
|
/**
|
|
589
645
|
* Capitalize first letter of string (e.g., 'slack' -> 'Slack')
|
|
590
646
|
*/
|
|
@@ -935,5 +935,100 @@ describe('IntegrationBuilder', () => {
|
|
|
935
935
|
);
|
|
936
936
|
});
|
|
937
937
|
});
|
|
938
|
+
|
|
939
|
+
describe('scoped environment (lambda.scopedEnvironment)', () => {
|
|
940
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
941
|
+
|
|
942
|
+
beforeEach(() => {
|
|
943
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
afterEach(() => {
|
|
947
|
+
if (originalSkipDiscovery === undefined) {
|
|
948
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
949
|
+
} else {
|
|
950
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const scopedApp = {
|
|
955
|
+
lambda: { scopedEnvironment: true },
|
|
956
|
+
adminScripts: [{ Definition: { name: 'fix-things' } }],
|
|
957
|
+
integrations: [
|
|
958
|
+
{ Definition: { name: 'hubspot', webhooks: true } },
|
|
959
|
+
{ Definition: { name: 'slack' } },
|
|
960
|
+
],
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
it('scopes queue URLs to auth, admin functions, and the owning integration only', async () => {
|
|
964
|
+
const result = await integrationBuilder.build(scopedApp, {});
|
|
965
|
+
|
|
966
|
+
expect(result.environment.HUBSPOT_QUEUE_URL).toBeUndefined();
|
|
967
|
+
expect(result.environment.SLACK_QUEUE_URL).toBeUndefined();
|
|
968
|
+
|
|
969
|
+
const scoped = result.functionEnvironments;
|
|
970
|
+
// auth and admin functions can enqueue to any integration
|
|
971
|
+
expect(scoped.auth).toEqual({
|
|
972
|
+
HUBSPOT_QUEUE_URL: { Ref: 'HubspotQueue' },
|
|
973
|
+
SLACK_QUEUE_URL: { Ref: 'SlackQueue' },
|
|
974
|
+
});
|
|
975
|
+
expect(scoped.adminScriptRouter.HUBSPOT_QUEUE_URL).toBeDefined();
|
|
976
|
+
expect(scoped.adminScriptExecutor.SLACK_QUEUE_URL).toBeDefined();
|
|
977
|
+
|
|
978
|
+
// owning integration's full function set
|
|
979
|
+
expect(scoped.hubspot.HUBSPOT_QUEUE_URL).toBeDefined();
|
|
980
|
+
expect(scoped.hubspotWebhook.HUBSPOT_QUEUE_URL).toBeDefined();
|
|
981
|
+
expect(scoped.hubspotQueueWorker.HUBSPOT_QUEUE_URL).toBeDefined();
|
|
982
|
+
|
|
983
|
+
// cross-integration isolation
|
|
984
|
+
expect(scoped.hubspotQueueWorker.SLACK_QUEUE_URL).toBeUndefined();
|
|
985
|
+
expect(scoped.slackQueueWorker.HUBSPOT_QUEUE_URL).toBeUndefined();
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('targets extension handler functions too', async () => {
|
|
989
|
+
const withExtension = {
|
|
990
|
+
lambda: { scopedEnvironment: true },
|
|
991
|
+
integrations: [
|
|
992
|
+
{
|
|
993
|
+
Definition: {
|
|
994
|
+
name: 'hubspot',
|
|
995
|
+
extensions: {
|
|
996
|
+
'my-ext': {
|
|
997
|
+
extension: {
|
|
998
|
+
routes: [{ path: '/x', method: 'GET' }],
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
],
|
|
1005
|
+
};
|
|
1006
|
+
const result = await integrationBuilder.build(withExtension, {});
|
|
1007
|
+
|
|
1008
|
+
expect(
|
|
1009
|
+
result.functionEnvironments.hubspot__myext.HUBSPOT_QUEUE_URL
|
|
1010
|
+
).toBeDefined();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('broadcasts app-wide when the flag is off', async () => {
|
|
1014
|
+
const result = await integrationBuilder.build(
|
|
1015
|
+
{ integrations: scopedApp.integrations },
|
|
1016
|
+
{}
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
expect(result.environment.HUBSPOT_QUEUE_URL).toEqual({
|
|
1020
|
+
Ref: 'HubspotQueue',
|
|
1021
|
+
});
|
|
1022
|
+
expect(result.functionEnvironments).toBeUndefined();
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('broadcasts in local mode even with the flag on', async () => {
|
|
1026
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
1027
|
+
const result = await integrationBuilder.build(scopedApp, {});
|
|
1028
|
+
|
|
1029
|
+
expect(result.environment.HUBSPOT_QUEUE_URL).toBeDefined();
|
|
1030
|
+
expect(result.functionEnvironments).toBeUndefined();
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
938
1033
|
});
|
|
939
1034
|
|
|
@@ -22,6 +22,7 @@ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-buil
|
|
|
22
22
|
const VpcResourceResolver = require('./vpc-resolver');
|
|
23
23
|
const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result');
|
|
24
24
|
const { ResourceOwnership } = require('../shared/types/resource-ownership');
|
|
25
|
+
const { isSsmOffloadActive } = require('../parameters/offload-utils');
|
|
25
26
|
|
|
26
27
|
class VpcBuilder extends InfrastructureBuilder {
|
|
27
28
|
constructor() {
|
|
@@ -177,6 +178,9 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
177
178
|
} else if (logicalId === 'FriggSQSVPCEndpoint' || logicalId === 'VPCEndpointSQS') {
|
|
178
179
|
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
179
180
|
physicalId = flatDiscovery.sqsVpcEndpointId;
|
|
181
|
+
} else if (logicalId === 'FriggSSMVPCEndpoint' || logicalId === 'VPCEndpointSSM') {
|
|
182
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
183
|
+
physicalId = flatDiscovery.ssmVpcEndpointId;
|
|
180
184
|
} else if (logicalId === 'FriggNATRoute' || logicalId === 'FriggPrivateRoute') {
|
|
181
185
|
resourceType = 'AWS::EC2::Route';
|
|
182
186
|
physicalId = flatDiscovery.natRoute;
|
|
@@ -296,6 +300,15 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
296
300
|
properties: { ServiceName: 'sqs' }
|
|
297
301
|
});
|
|
298
302
|
}
|
|
303
|
+
|
|
304
|
+
if (flatDiscovery.ssmVpcEndpointId && typeof flatDiscovery.ssmVpcEndpointId === 'string') {
|
|
305
|
+
discovery.external.push({
|
|
306
|
+
physicalId: flatDiscovery.ssmVpcEndpointId,
|
|
307
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
308
|
+
source: 'aws-discovery',
|
|
309
|
+
properties: { ServiceName: 'ssm' }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
299
312
|
}
|
|
300
313
|
|
|
301
314
|
// Add flat discovery properties directly to discovery object for resolver access
|
|
@@ -1005,7 +1018,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1005
1018
|
}
|
|
1006
1019
|
|
|
1007
1020
|
// Create security group for interface endpoints if needed
|
|
1008
|
-
const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
|
|
1021
|
+
const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs', 'ssm'].includes(type));
|
|
1009
1022
|
if (needsInterfaceEndpoints) {
|
|
1010
1023
|
// Determine source security group for ingress rule
|
|
1011
1024
|
let sourceSgId;
|
|
@@ -1080,6 +1093,20 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1080
1093
|
};
|
|
1081
1094
|
}
|
|
1082
1095
|
|
|
1096
|
+
if (endpointsToCreate.includes('ssm')) {
|
|
1097
|
+
result.resources.FriggSSMVPCEndpoint = {
|
|
1098
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1099
|
+
Properties: {
|
|
1100
|
+
VpcId: vpcId,
|
|
1101
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.ssm',
|
|
1102
|
+
VpcEndpointType: 'Interface',
|
|
1103
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
1104
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
1105
|
+
PrivateDnsEnabled: true,
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1083
1110
|
console.log(` ✅ VPC Endpoint resources added to template`);
|
|
1084
1111
|
}
|
|
1085
1112
|
|
|
@@ -1157,7 +1184,8 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1157
1184
|
dynamodb: existingLogicalIds.includes('VPCEndpointDynamoDB') ? 'VPCEndpointDynamoDB' : 'FriggDynamoDBVPCEndpoint',
|
|
1158
1185
|
kms: existingLogicalIds.includes('VPCEndpointKMS') ? 'VPCEndpointKMS' : 'FriggKMSVPCEndpoint',
|
|
1159
1186
|
secretsManager: existingLogicalIds.includes('VPCEndpointSecretsManager') ? 'VPCEndpointSecretsManager' : 'FriggSecretsManagerVPCEndpoint',
|
|
1160
|
-
sqs: existingLogicalIds.includes('VPCEndpointSQS') ? 'VPCEndpointSQS' : 'FriggSQSVPCEndpoint'
|
|
1187
|
+
sqs: existingLogicalIds.includes('VPCEndpointSQS') ? 'VPCEndpointSQS' : 'FriggSQSVPCEndpoint',
|
|
1188
|
+
ssm: existingLogicalIds.includes('VPCEndpointSSM') ? 'VPCEndpointSSM' : 'FriggSSMVPCEndpoint'
|
|
1161
1189
|
};
|
|
1162
1190
|
|
|
1163
1191
|
Object.entries(decisions).forEach(([type, decision]) => {
|
|
@@ -1186,11 +1214,12 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1186
1214
|
}
|
|
1187
1215
|
};
|
|
1188
1216
|
} else {
|
|
1189
|
-
// Interface endpoints (KMS, Secrets Manager, SQS)
|
|
1217
|
+
// Interface endpoints (KMS, Secrets Manager, SQS, SSM)
|
|
1190
1218
|
const serviceMap = {
|
|
1191
1219
|
kms: 'kms',
|
|
1192
1220
|
secretsManager: 'secretsmanager',
|
|
1193
|
-
sqs: 'sqs'
|
|
1221
|
+
sqs: 'sqs',
|
|
1222
|
+
ssm: 'ssm'
|
|
1194
1223
|
};
|
|
1195
1224
|
|
|
1196
1225
|
result.resources[logicalId] = {
|
|
@@ -1209,7 +1238,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1209
1238
|
});
|
|
1210
1239
|
|
|
1211
1240
|
// If any interface endpoints exist, ensure security group is in template
|
|
1212
|
-
const hasInterfaceEndpoints = ['kms', 'secretsManager', 'sqs'].some(
|
|
1241
|
+
const hasInterfaceEndpoints = ['kms', 'secretsManager', 'sqs', 'ssm'].some(
|
|
1213
1242
|
type => decisions[type]?.ownership === ResourceOwnership.STACK && decisions[type]?.physicalId
|
|
1214
1243
|
);
|
|
1215
1244
|
|
|
@@ -1887,8 +1916,11 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1887
1916
|
kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
|
|
1888
1917
|
secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
|
|
1889
1918
|
sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',
|
|
1919
|
+
ssm: discoveredResources.ssmVpcEndpointId && typeof discoveredResources.ssmVpcEndpointId === 'string',
|
|
1890
1920
|
};
|
|
1891
1921
|
|
|
1922
|
+
const needsSsm = isSsmOffloadActive(appDefinition);
|
|
1923
|
+
|
|
1892
1924
|
// Build list of what needs creation (not stack-managed, not existing elsewhere)
|
|
1893
1925
|
const missing = [];
|
|
1894
1926
|
if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) missing.push('S3');
|
|
@@ -1897,6 +1929,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1897
1929
|
if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) missing.push('Secrets Manager');
|
|
1898
1930
|
// SQS endpoint needed for job queues and async processing
|
|
1899
1931
|
if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) missing.push('SQS');
|
|
1932
|
+
if (!stackManagedEndpoints.ssm && !existingEndpoints.ssm && needsSsm) missing.push('SSM');
|
|
1900
1933
|
|
|
1901
1934
|
// Log reused stack-managed endpoints
|
|
1902
1935
|
const reused = [];
|
|
@@ -1905,6 +1938,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1905
1938
|
if (stackManagedEndpoints.kms) reused.push('KMS');
|
|
1906
1939
|
if (stackManagedEndpoints.secretsManager) reused.push('Secrets Manager');
|
|
1907
1940
|
if (stackManagedEndpoints.sqs) reused.push('SQS');
|
|
1941
|
+
if (stackManagedEndpoints.ssm) reused.push('SSM');
|
|
1908
1942
|
|
|
1909
1943
|
if (reused.length > 0) {
|
|
1910
1944
|
console.log(` ✓ Reusing stack-managed VPC endpoints: ${reused.join(', ')}`);
|
|
@@ -1968,11 +2002,12 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1968
2002
|
};
|
|
1969
2003
|
}
|
|
1970
2004
|
|
|
1971
|
-
// VPC Endpoint Security Group (only if KMS, Secrets Manager, or
|
|
2005
|
+
// VPC Endpoint Security Group (only if KMS, Secrets Manager, SQS, or SSM are not stack-managed and missing)
|
|
1972
2006
|
const needsSecurityGroup =
|
|
1973
2007
|
(!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') ||
|
|
1974
2008
|
(!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) ||
|
|
1975
|
-
(!stackManagedEndpoints.sqs && !existingEndpoints.sqs)
|
|
2009
|
+
(!stackManagedEndpoints.sqs && !existingEndpoints.sqs) ||
|
|
2010
|
+
(!stackManagedEndpoints.ssm && !existingEndpoints.ssm && needsSsm);
|
|
1976
2011
|
|
|
1977
2012
|
if (needsSecurityGroup) {
|
|
1978
2013
|
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
@@ -2043,6 +2078,21 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
2043
2078
|
};
|
|
2044
2079
|
}
|
|
2045
2080
|
|
|
2081
|
+
// SSM Interface Endpoint (only if not stack-managed, missing, AND SSM offload is active)
|
|
2082
|
+
if (!stackManagedEndpoints.ssm && !existingEndpoints.ssm && needsSsm) {
|
|
2083
|
+
result.resources.FriggSSMVPCEndpoint = {
|
|
2084
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
2085
|
+
Properties: {
|
|
2086
|
+
VpcId: vpcId,
|
|
2087
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.ssm',
|
|
2088
|
+
VpcEndpointType: 'Interface',
|
|
2089
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
2090
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
2091
|
+
PrivateDnsEnabled: true,
|
|
2092
|
+
},
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2046
2096
|
console.log(` ✅ Created ${missing.length} VPC endpoint(s): ${missing.join(', ')}`);
|
|
2047
2097
|
}
|
|
2048
2098
|
}
|
|
@@ -990,6 +990,47 @@ describe('VpcBuilder', () => {
|
|
|
990
990
|
});
|
|
991
991
|
});
|
|
992
992
|
|
|
993
|
+
describe('SSM VPC Endpoint', () => {
|
|
994
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
995
|
+
|
|
996
|
+
afterEach(() => {
|
|
997
|
+
if (originalSkipDiscovery === undefined) {
|
|
998
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
999
|
+
} else {
|
|
1000
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should create SSM endpoint when offload is active and VPC is enabled', async () => {
|
|
1005
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
1006
|
+
const appDefinition = {
|
|
1007
|
+
vpc: { enable: true, management: 'create-new' },
|
|
1008
|
+
ssm: { enable: true },
|
|
1009
|
+
environment: { FOO: 'ssm' },
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
const result = await vpcBuilder.build(appDefinition, {});
|
|
1013
|
+
|
|
1014
|
+
expect(result.resources.FriggSSMVPCEndpoint).toBeDefined();
|
|
1015
|
+
expect(result.resources.FriggSSMVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
1016
|
+
expect(result.resources.FriggSSMVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
|
|
1017
|
+
expect(result.resources.FriggSSMVPCEndpoint.Properties.ServiceName).toBe('com.amazonaws.${self:provider.region}.ssm');
|
|
1018
|
+
expect(result.resources.FriggSSMVPCEndpoint.Properties.PrivateDnsEnabled).toBe(true);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it('should not create SSM endpoint when offload is not active', async () => {
|
|
1022
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
1023
|
+
const appDefinition = {
|
|
1024
|
+
vpc: { enable: true, management: 'create-new' },
|
|
1025
|
+
ssm: { enable: true },
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const result = await vpcBuilder.build(appDefinition, {});
|
|
1029
|
+
|
|
1030
|
+
expect(result.resources.FriggSSMVPCEndpoint).toBeUndefined();
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
|
|
993
1034
|
describe('Self-healing', () => {
|
|
994
1035
|
it('should create missing subnets when selfHeal is enabled', async () => {
|
|
995
1036
|
const appDefinition = {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const BaseResourceResolver = require('../shared/base-resolver');
|
|
15
15
|
const { ResourceOwnership } = require('../shared/types');
|
|
16
|
+
const { isSsmOffloadActive } = require('../parameters/offload-utils');
|
|
16
17
|
|
|
17
18
|
class VpcResourceResolver extends BaseResourceResolver {
|
|
18
19
|
/**
|
|
@@ -381,7 +382,8 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
381
382
|
dynamodb: { ownership: null, reason: 'VPC Endpoints disabled' },
|
|
382
383
|
kms: { ownership: null, reason: 'VPC Endpoints disabled' },
|
|
383
384
|
secretsManager: { ownership: null, reason: 'VPC Endpoints disabled' },
|
|
384
|
-
sqs: { ownership: null, reason: 'VPC Endpoints disabled' }
|
|
385
|
+
sqs: { ownership: null, reason: 'VPC Endpoints disabled' },
|
|
386
|
+
ssm: { ownership: null, reason: 'VPC Endpoints disabled' }
|
|
385
387
|
};
|
|
386
388
|
}
|
|
387
389
|
|
|
@@ -389,6 +391,9 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
389
391
|
const encryptionMethod = appDefinition.encryption?.fieldLevelEncryptionMethod;
|
|
390
392
|
const needsKms = encryptionMethod === 'kms';
|
|
391
393
|
|
|
394
|
+
// SSM endpoint only needed when parameter offload is active
|
|
395
|
+
const needsSsm = isSsmOffloadActive(appDefinition);
|
|
396
|
+
|
|
392
397
|
// DynamoDB endpoint only needed if using DynamoDB (not MongoDB or PostgreSQL)
|
|
393
398
|
// Currently framework only supports MongoDB (via Prisma) and PostgreSQL (via Aurora)
|
|
394
399
|
// If not using DynamoDB, skip the endpoint (CloudFormation will delete if it exists)
|
|
@@ -403,7 +408,10 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
403
408
|
? this._resolveEndpoint('FriggKMSVPCEndpoint', 'kms', userIntent, appDefinition, discovery)
|
|
404
409
|
: { ownership: null, reason: 'KMS endpoint not needed (encryption method is not KMS)' },
|
|
405
410
|
secretsManager: this._resolveEndpoint('FriggSecretsManagerVPCEndpoint', 'secretsManager', userIntent, appDefinition, discovery),
|
|
406
|
-
sqs: this._resolveEndpoint('FriggSQSVPCEndpoint', 'sqs', userIntent, appDefinition, discovery)
|
|
411
|
+
sqs: this._resolveEndpoint('FriggSQSVPCEndpoint', 'sqs', userIntent, appDefinition, discovery),
|
|
412
|
+
ssm: needsSsm
|
|
413
|
+
? this._resolveEndpoint('FriggSSMVPCEndpoint', 'ssm', userIntent, appDefinition, discovery)
|
|
414
|
+
: { ownership: null, reason: 'SSM endpoint not needed (SSM offload not active)' }
|
|
407
415
|
};
|
|
408
416
|
|
|
409
417
|
return endpoints;
|
|
@@ -421,7 +429,8 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
421
429
|
'FriggDynamoDBVPCEndpoint': 'VPCEndpointDynamoDB',
|
|
422
430
|
'FriggKMSVPCEndpoint': 'VPCEndpointKMS',
|
|
423
431
|
'FriggSecretsManagerVPCEndpoint': 'VPCEndpointSecretsManager',
|
|
424
|
-
'FriggSQSVPCEndpoint': 'VPCEndpointSQS'
|
|
432
|
+
'FriggSQSVPCEndpoint': 'VPCEndpointSQS',
|
|
433
|
+
'FriggSSMVPCEndpoint': 'VPCEndpointSSM'
|
|
425
434
|
};
|
|
426
435
|
const oldLogicalId = oldLogicalIdMap[logicalId];
|
|
427
436
|
|
|
@@ -567,6 +567,46 @@ describe('VpcResourceResolver', () => {
|
|
|
567
567
|
expect(decisions.kms.ownership).toBeNull();
|
|
568
568
|
expect(decisions.secretsManager.ownership).toBeNull();
|
|
569
569
|
expect(decisions.sqs.ownership).toBeNull();
|
|
570
|
+
expect(decisions.ssm.ownership).toBeNull();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe('SSM endpoint', () => {
|
|
574
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
575
|
+
|
|
576
|
+
afterEach(() => {
|
|
577
|
+
if (originalSkipDiscovery === undefined) {
|
|
578
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
579
|
+
} else {
|
|
580
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should create SSM endpoint when offload is active', () => {
|
|
585
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
586
|
+
const appDefinition = {
|
|
587
|
+
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
588
|
+
ssm: { enable: true },
|
|
589
|
+
environment: { FOO: 'ssm' }
|
|
590
|
+
};
|
|
591
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
592
|
+
|
|
593
|
+
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
594
|
+
|
|
595
|
+
expect(decisions.ssm.ownership).toBe('stack');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should not create SSM endpoint when offload is not active', () => {
|
|
599
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
600
|
+
const appDefinition = {
|
|
601
|
+
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
602
|
+
ssm: { enable: true }
|
|
603
|
+
};
|
|
604
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
605
|
+
|
|
606
|
+
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
607
|
+
|
|
608
|
+
expect(decisions.ssm.ownership).toBeNull();
|
|
609
|
+
});
|
|
570
610
|
});
|
|
571
611
|
|
|
572
612
|
it('should resolve to EXTERNAL with user-provided endpoint IDs', () => {
|