@friggframework/devtools 2.0.0--canary.461.8cf93ae.0 → 2.0.0--canary.474.aa465e4.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/deploy.test.js +0 -39
- package/frigg-cli/deploy-command/index.js +0 -5
- package/frigg-cli/index.js +0 -1
- package/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/domains/database/aurora-builder.js +234 -57
- package/infrastructure/domains/database/aurora-builder.test.js +7 -2
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +256 -215
- package/infrastructure/domains/database/migration-builder.test.js +5 -111
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/integration/integration-builder.js +258 -84
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/networking/vpc-builder.js +856 -135
- package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/security/kms-builder.js +179 -22
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/package.json +6 -6
- package/infrastructure/REFACTOR.md +0 -532
- package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Discovery result types
|
|
3
|
+
*
|
|
4
|
+
* Defines the structure of resource discovery results.
|
|
5
|
+
* Discovery reports FACTS - it doesn't make ownership decisions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resource that exists in our CloudFormation stack
|
|
10
|
+
* @typedef {Object} StackManagedResource
|
|
11
|
+
* @property {string} logicalId - CloudFormation logical resource ID (e.g., 'FriggLambdaSecurityGroup')
|
|
12
|
+
* @property {string} physicalId - AWS physical resource ID (e.g., 'sg-069629001ade41c9a')
|
|
13
|
+
* @property {string} resourceType - CloudFormation resource type (e.g., 'AWS::EC2::SecurityGroup')
|
|
14
|
+
* @property {Object} [properties] - Resource properties (optional, for detailed info)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resource that exists outside our CloudFormation stack
|
|
19
|
+
* @typedef {Object} ExternalResource
|
|
20
|
+
* @property {string} physicalId - AWS physical resource ID
|
|
21
|
+
* @property {string} resourceType - CloudFormation resource type
|
|
22
|
+
* @property {'tag-search' | 'name-search' | 'aws-api' | 'user-provided'} source - How it was discovered
|
|
23
|
+
* @property {Object} [properties] - Resource properties (optional)
|
|
24
|
+
* @property {Object} [tags] - Resource tags (if available)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Complete discovery result
|
|
29
|
+
* @typedef {Object} DiscoveryResult
|
|
30
|
+
* @property {StackManagedResource[]} stackManaged - Resources in OUR CloudFormation stack
|
|
31
|
+
* @property {ExternalResource[]} external - Resources outside our stack
|
|
32
|
+
* @property {string} [stackName] - Name of our CloudFormation stack (if exists)
|
|
33
|
+
* @property {boolean} fromCloudFormation - Whether stack was found in CloudFormation
|
|
34
|
+
* @property {string} [region] - AWS region
|
|
35
|
+
* @property {Object} [metadata] - Additional discovery metadata
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create empty discovery result
|
|
40
|
+
* @returns {DiscoveryResult}
|
|
41
|
+
*/
|
|
42
|
+
function createEmptyDiscoveryResult() {
|
|
43
|
+
return {
|
|
44
|
+
stackManaged: [],
|
|
45
|
+
external: [],
|
|
46
|
+
fromCloudFormation: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find stack-managed resource by logical ID
|
|
52
|
+
* @param {DiscoveryResult} discovery - Discovery result
|
|
53
|
+
* @param {string} logicalId - Logical resource ID to find
|
|
54
|
+
* @returns {StackManagedResource|null}
|
|
55
|
+
*/
|
|
56
|
+
function findStackResource(discovery, logicalId) {
|
|
57
|
+
return discovery.stackManaged.find(r => r.logicalId === logicalId) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Find external resource by type
|
|
62
|
+
* @param {DiscoveryResult} discovery - Discovery result
|
|
63
|
+
* @param {string} resourceType - CloudFormation resource type
|
|
64
|
+
* @returns {ExternalResource|null}
|
|
65
|
+
*/
|
|
66
|
+
function findExternalResource(discovery, resourceType) {
|
|
67
|
+
return discovery.external.find(r => r.resourceType === resourceType) || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find all external resources by type
|
|
72
|
+
* @param {DiscoveryResult} discovery - Discovery result
|
|
73
|
+
* @param {string} resourceType - CloudFormation resource type
|
|
74
|
+
* @returns {ExternalResource[]}
|
|
75
|
+
*/
|
|
76
|
+
function findAllExternalResources(discovery, resourceType) {
|
|
77
|
+
return discovery.external.filter(r => r.resourceType === resourceType);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if specific resource exists in stack
|
|
82
|
+
* @param {DiscoveryResult} discovery - Discovery result
|
|
83
|
+
* @param {string} logicalId - Logical resource ID
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function isResourceInStack(discovery, logicalId) {
|
|
87
|
+
return discovery.stackManaged.some(r => r.logicalId === logicalId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all stack logical IDs
|
|
92
|
+
* @param {DiscoveryResult} discovery - Discovery result
|
|
93
|
+
* @returns {string[]}
|
|
94
|
+
*/
|
|
95
|
+
function getStackLogicalIds(discovery) {
|
|
96
|
+
return discovery.stackManaged.map(r => r.logicalId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
createEmptyDiscoveryResult,
|
|
101
|
+
findStackResource,
|
|
102
|
+
findExternalResource,
|
|
103
|
+
findAllExternalResources,
|
|
104
|
+
isResourceInStack,
|
|
105
|
+
getStackLogicalIds
|
|
106
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createEmptyDiscoveryResult,
|
|
3
|
+
findStackResource,
|
|
4
|
+
findExternalResource,
|
|
5
|
+
findAllExternalResources,
|
|
6
|
+
isResourceInStack,
|
|
7
|
+
getStackLogicalIds
|
|
8
|
+
} = require('./discovery-result');
|
|
9
|
+
|
|
10
|
+
describe('Discovery Result Utilities', () => {
|
|
11
|
+
describe('createEmptyDiscoveryResult', () => {
|
|
12
|
+
it('should create empty discovery result with correct structure', () => {
|
|
13
|
+
const result = createEmptyDiscoveryResult();
|
|
14
|
+
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
stackManaged: [],
|
|
17
|
+
external: [],
|
|
18
|
+
fromCloudFormation: false
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('findStackResource', () => {
|
|
24
|
+
it('should find resource by logical ID', () => {
|
|
25
|
+
const discovery = {
|
|
26
|
+
stackManaged: [
|
|
27
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
|
|
28
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' }
|
|
29
|
+
],
|
|
30
|
+
external: [],
|
|
31
|
+
fromCloudFormation: true
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const found = findStackResource(discovery, 'FriggLambdaSecurityGroup');
|
|
35
|
+
|
|
36
|
+
expect(found).toEqual({
|
|
37
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
38
|
+
physicalId: 'sg-456',
|
|
39
|
+
resourceType: 'AWS::EC2::SecurityGroup'
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return null if resource not found', () => {
|
|
44
|
+
const discovery = {
|
|
45
|
+
stackManaged: [
|
|
46
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' }
|
|
47
|
+
],
|
|
48
|
+
external: [],
|
|
49
|
+
fromCloudFormation: true
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const found = findStackResource(discovery, 'NonExistent');
|
|
53
|
+
|
|
54
|
+
expect(found).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return null for empty stack managed resources', () => {
|
|
58
|
+
const discovery = createEmptyDiscoveryResult();
|
|
59
|
+
const found = findStackResource(discovery, 'FriggVPC');
|
|
60
|
+
|
|
61
|
+
expect(found).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('findExternalResource', () => {
|
|
66
|
+
it('should find external resource by type', () => {
|
|
67
|
+
const discovery = {
|
|
68
|
+
stackManaged: [],
|
|
69
|
+
external: [
|
|
70
|
+
{ physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'tag-search' },
|
|
71
|
+
{ physicalId: 'sg-external', resourceType: 'AWS::EC2::SecurityGroup', source: 'tag-search' }
|
|
72
|
+
],
|
|
73
|
+
fromCloudFormation: false
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const found = findExternalResource(discovery, 'AWS::EC2::SecurityGroup');
|
|
77
|
+
|
|
78
|
+
expect(found).toEqual({
|
|
79
|
+
physicalId: 'sg-external',
|
|
80
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
81
|
+
source: 'tag-search'
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return null if external resource not found', () => {
|
|
86
|
+
const discovery = createEmptyDiscoveryResult();
|
|
87
|
+
const found = findExternalResource(discovery, 'AWS::EC2::VPC');
|
|
88
|
+
|
|
89
|
+
expect(found).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return first match if multiple resources of same type', () => {
|
|
93
|
+
const discovery = {
|
|
94
|
+
stackManaged: [],
|
|
95
|
+
external: [
|
|
96
|
+
{ physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
|
|
97
|
+
{ physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' }
|
|
98
|
+
],
|
|
99
|
+
fromCloudFormation: false
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const found = findExternalResource(discovery, 'AWS::EC2::Subnet');
|
|
103
|
+
|
|
104
|
+
expect(found.physicalId).toBe('subnet-1');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('findAllExternalResources', () => {
|
|
109
|
+
it('should find all external resources of given type', () => {
|
|
110
|
+
const discovery = {
|
|
111
|
+
stackManaged: [],
|
|
112
|
+
external: [
|
|
113
|
+
{ physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
|
|
114
|
+
{ physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
|
|
115
|
+
{ physicalId: 'sg-1', resourceType: 'AWS::EC2::SecurityGroup', source: 'tag-search' }
|
|
116
|
+
],
|
|
117
|
+
fromCloudFormation: false
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const found = findAllExternalResources(discovery, 'AWS::EC2::Subnet');
|
|
121
|
+
|
|
122
|
+
expect(found).toHaveLength(2);
|
|
123
|
+
expect(found[0].physicalId).toBe('subnet-1');
|
|
124
|
+
expect(found[1].physicalId).toBe('subnet-2');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should return empty array if no resources found', () => {
|
|
128
|
+
const discovery = createEmptyDiscoveryResult();
|
|
129
|
+
const found = findAllExternalResources(discovery, 'AWS::EC2::VPC');
|
|
130
|
+
|
|
131
|
+
expect(found).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('isResourceInStack', () => {
|
|
136
|
+
it('should return true if resource is in stack', () => {
|
|
137
|
+
const discovery = {
|
|
138
|
+
stackManaged: [
|
|
139
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
|
|
140
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' }
|
|
141
|
+
],
|
|
142
|
+
external: [],
|
|
143
|
+
fromCloudFormation: true
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
expect(isResourceInStack(discovery, 'FriggVPC')).toBe(true);
|
|
147
|
+
expect(isResourceInStack(discovery, 'FriggLambdaSecurityGroup')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should return false if resource is not in stack', () => {
|
|
151
|
+
const discovery = {
|
|
152
|
+
stackManaged: [
|
|
153
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' }
|
|
154
|
+
],
|
|
155
|
+
external: [],
|
|
156
|
+
fromCloudFormation: true
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
expect(isResourceInStack(discovery, 'NonExistent')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return false for empty stack', () => {
|
|
163
|
+
const discovery = createEmptyDiscoveryResult();
|
|
164
|
+
|
|
165
|
+
expect(isResourceInStack(discovery, 'FriggVPC')).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getStackLogicalIds', () => {
|
|
170
|
+
it('should return all logical IDs from stack', () => {
|
|
171
|
+
const discovery = {
|
|
172
|
+
stackManaged: [
|
|
173
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
|
|
174
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
175
|
+
{ logicalId: 'FriggAuroraCluster', physicalId: 'cluster-789', resourceType: 'AWS::RDS::DBCluster' }
|
|
176
|
+
],
|
|
177
|
+
external: [],
|
|
178
|
+
fromCloudFormation: true
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const ids = getStackLogicalIds(discovery);
|
|
182
|
+
|
|
183
|
+
expect(ids).toEqual([
|
|
184
|
+
'FriggVPC',
|
|
185
|
+
'FriggLambdaSecurityGroup',
|
|
186
|
+
'FriggAuroraCluster'
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should return empty array for empty stack', () => {
|
|
191
|
+
const discovery = createEmptyDiscoveryResult();
|
|
192
|
+
const ids = getStackLogicalIds(discovery);
|
|
193
|
+
|
|
194
|
+
expect(ids).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('real-world scenarios', () => {
|
|
199
|
+
it('scenario: fresh deploy, no stack exists', () => {
|
|
200
|
+
const discovery = createEmptyDiscoveryResult();
|
|
201
|
+
|
|
202
|
+
expect(discovery.fromCloudFormation).toBe(false);
|
|
203
|
+
expect(discovery.stackManaged).toHaveLength(0);
|
|
204
|
+
expect(discovery.external).toHaveLength(0);
|
|
205
|
+
|
|
206
|
+
expect(isResourceInStack(discovery, 'FriggVPC')).toBe(false);
|
|
207
|
+
expect(findStackResource(discovery, 'FriggVPC')).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('scenario: redeploy existing stack', () => {
|
|
211
|
+
const discovery = {
|
|
212
|
+
stackManaged: [
|
|
213
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
|
|
214
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
215
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
|
|
216
|
+
{ logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' },
|
|
217
|
+
{ logicalId: 'FriggAuroraCluster', physicalId: 'cluster-abc', resourceType: 'AWS::RDS::DBCluster' },
|
|
218
|
+
{ logicalId: 'FriggKMSKey', physicalId: 'key-xyz', resourceType: 'AWS::KMS::Key' }
|
|
219
|
+
],
|
|
220
|
+
external: [],
|
|
221
|
+
fromCloudFormation: true,
|
|
222
|
+
stackName: 'create-frigg-app-production'
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
expect(discovery.fromCloudFormation).toBe(true);
|
|
226
|
+
expect(discovery.stackManaged).toHaveLength(6);
|
|
227
|
+
|
|
228
|
+
// All these resources are in stack - MUST be kept in template
|
|
229
|
+
expect(isResourceInStack(discovery, 'FriggLambdaSecurityGroup')).toBe(true);
|
|
230
|
+
expect(isResourceInStack(discovery, 'FriggAuroraCluster')).toBe(true);
|
|
231
|
+
|
|
232
|
+
const sg = findStackResource(discovery, 'FriggLambdaSecurityGroup');
|
|
233
|
+
expect(sg.physicalId).toBe('sg-069629001ade41c9a');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('scenario: use external VPC with stack-managed resources', () => {
|
|
237
|
+
const discovery = {
|
|
238
|
+
stackManaged: [
|
|
239
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
240
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' }
|
|
241
|
+
],
|
|
242
|
+
external: [
|
|
243
|
+
{ physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'user-provided' }
|
|
244
|
+
],
|
|
245
|
+
fromCloudFormation: true,
|
|
246
|
+
stackName: 'my-app-dev'
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// VPC is external - should NOT be in template
|
|
250
|
+
expect(isResourceInStack(discovery, 'FriggVPC')).toBe(false);
|
|
251
|
+
const externalVpc = findExternalResource(discovery, 'AWS::EC2::VPC');
|
|
252
|
+
expect(externalVpc.physicalId).toBe('vpc-external');
|
|
253
|
+
|
|
254
|
+
// But security group IS in stack - MUST be in template
|
|
255
|
+
expect(isResourceInStack(discovery, 'FriggLambdaSecurityGroup')).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Central export for all infrastructure types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { ResourceOwnership, validateOwnership, resolveOwnership } = require('./resource-ownership');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
createEmptyDiscoveryResult,
|
|
9
|
+
findStackResource,
|
|
10
|
+
findExternalResource,
|
|
11
|
+
findAllExternalResources,
|
|
12
|
+
isResourceInStack,
|
|
13
|
+
getStackLogicalIds
|
|
14
|
+
} = require('./discovery-result');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
validateAppDefinition,
|
|
18
|
+
getStackName,
|
|
19
|
+
isVpcEnabled,
|
|
20
|
+
isAuroraEnabled,
|
|
21
|
+
isKmsEnabled,
|
|
22
|
+
isSsmEnabled
|
|
23
|
+
} = require('./app-definition');
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
// Resource Ownership
|
|
27
|
+
ResourceOwnership,
|
|
28
|
+
validateOwnership,
|
|
29
|
+
resolveOwnership,
|
|
30
|
+
|
|
31
|
+
// Discovery Result
|
|
32
|
+
createEmptyDiscoveryResult,
|
|
33
|
+
findStackResource,
|
|
34
|
+
findExternalResource,
|
|
35
|
+
findAllExternalResources,
|
|
36
|
+
isResourceInStack,
|
|
37
|
+
getStackLogicalIds,
|
|
38
|
+
|
|
39
|
+
// App Definition
|
|
40
|
+
validateAppDefinition,
|
|
41
|
+
getStackName,
|
|
42
|
+
isVpcEnabled,
|
|
43
|
+
isAuroraEnabled,
|
|
44
|
+
isKmsEnabled,
|
|
45
|
+
isSsmEnabled
|
|
46
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Resource ownership types for clean architecture
|
|
3
|
+
*
|
|
4
|
+
* This file defines the core types for the three-layer resource architecture:
|
|
5
|
+
* 1. Ownership (STACK | EXTERNAL | AUTO)
|
|
6
|
+
* 2. Discovery (facts about what exists)
|
|
7
|
+
* 3. Resolution (decisions based on ownership + discovery)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resource ownership states
|
|
12
|
+
* @readonly
|
|
13
|
+
* @enum {string}
|
|
14
|
+
*/
|
|
15
|
+
const ResourceOwnership = {
|
|
16
|
+
/**
|
|
17
|
+
* Resource is managed by our CloudFormation stack
|
|
18
|
+
* CRITICAL: Must be added to template on every deploy or CloudFormation will delete it
|
|
19
|
+
*/
|
|
20
|
+
STACK: 'stack',
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resource exists outside our stack (another stack, manually created, etc.)
|
|
24
|
+
* Should NOT be added to our template - reference by physical ID only
|
|
25
|
+
*/
|
|
26
|
+
EXTERNAL: 'external',
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Let the system decide based on discovery results
|
|
30
|
+
* - If found in our stack → STACK
|
|
31
|
+
* - If found externally → EXTERNAL
|
|
32
|
+
* - If not found → STACK (create new)
|
|
33
|
+
*/
|
|
34
|
+
AUTO: 'auto'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resource decision made by resolver
|
|
39
|
+
* @typedef {Object} ResourceDecision
|
|
40
|
+
* @property {string} ownership - The ownership decision (STACK | EXTERNAL)
|
|
41
|
+
* @property {string} [physicalId] - Physical resource ID (for EXTERNAL or existing STACK resources)
|
|
42
|
+
* @property {string[]} [physicalIds] - Multiple physical IDs (e.g., security groups, subnets)
|
|
43
|
+
* @property {string} reason - Human-readable reason for this decision
|
|
44
|
+
* @property {Object} [metadata] - Additional metadata about the resource
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ownership intent from app definition
|
|
49
|
+
* @typedef {'stack' | 'external' | 'auto'} OwnershipIntent
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate ownership value
|
|
54
|
+
* @param {string} value - Value to validate
|
|
55
|
+
* @param {string} resourceName - Name of resource for error message
|
|
56
|
+
* @throws {Error} If value is not valid ownership
|
|
57
|
+
*/
|
|
58
|
+
function validateOwnership(value, resourceName) {
|
|
59
|
+
const validValues = Object.values(ResourceOwnership);
|
|
60
|
+
if (!validValues.includes(value)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Invalid ownership '${value}' for ${resourceName}. Must be one of: ${validValues.join(', ')}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve final ownership from intent and actual state
|
|
69
|
+
* @param {OwnershipIntent} intent - User's intent from app definition
|
|
70
|
+
* @param {boolean} inStack - Whether resource is in our CloudFormation stack
|
|
71
|
+
* @param {boolean} foundExternal - Whether resource was found externally
|
|
72
|
+
* @returns {string} Final ownership (STACK | EXTERNAL)
|
|
73
|
+
*/
|
|
74
|
+
function resolveOwnership(intent, inStack, foundExternal) {
|
|
75
|
+
// Explicit stack
|
|
76
|
+
if (intent === ResourceOwnership.STACK) {
|
|
77
|
+
return ResourceOwnership.STACK;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Explicit external
|
|
81
|
+
if (intent === ResourceOwnership.EXTERNAL) {
|
|
82
|
+
return ResourceOwnership.EXTERNAL;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Auto-decide
|
|
86
|
+
if (intent === ResourceOwnership.AUTO) {
|
|
87
|
+
// CRITICAL: If it's in our stack, it MUST stay in template
|
|
88
|
+
if (inStack) {
|
|
89
|
+
return ResourceOwnership.STACK;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Found externally - use it
|
|
93
|
+
if (foundExternal) {
|
|
94
|
+
return ResourceOwnership.EXTERNAL;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Not found - create new in stack
|
|
98
|
+
return ResourceOwnership.STACK;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(`Invalid ownership intent: ${intent}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
ResourceOwnership,
|
|
106
|
+
validateOwnership,
|
|
107
|
+
resolveOwnership
|
|
108
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { ResourceOwnership, validateOwnership, resolveOwnership } = require('./resource-ownership');
|
|
2
|
+
|
|
3
|
+
describe('ResourceOwnership', () => {
|
|
4
|
+
describe('enum values', () => {
|
|
5
|
+
it('should have correct enum values', () => {
|
|
6
|
+
expect(ResourceOwnership.STACK).toBe('stack');
|
|
7
|
+
expect(ResourceOwnership.EXTERNAL).toBe('external');
|
|
8
|
+
expect(ResourceOwnership.AUTO).toBe('auto');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('validateOwnership', () => {
|
|
13
|
+
it('should accept valid ownership values', () => {
|
|
14
|
+
expect(() => validateOwnership('stack', 'test')).not.toThrow();
|
|
15
|
+
expect(() => validateOwnership('external', 'test')).not.toThrow();
|
|
16
|
+
expect(() => validateOwnership('auto', 'test')).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should reject invalid ownership values', () => {
|
|
20
|
+
expect(() => validateOwnership('invalid', 'test')).toThrow(
|
|
21
|
+
"Invalid ownership 'invalid' for test"
|
|
22
|
+
);
|
|
23
|
+
expect(() => validateOwnership('managed', 'test')).toThrow();
|
|
24
|
+
expect(() => validateOwnership('', 'test')).toThrow();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('resolveOwnership', () => {
|
|
29
|
+
describe('with explicit STACK intent', () => {
|
|
30
|
+
it('should return STACK regardless of discovery', () => {
|
|
31
|
+
expect(resolveOwnership('stack', false, false)).toBe('stack');
|
|
32
|
+
expect(resolveOwnership('stack', true, false)).toBe('stack');
|
|
33
|
+
expect(resolveOwnership('stack', false, true)).toBe('stack');
|
|
34
|
+
expect(resolveOwnership('stack', true, true)).toBe('stack');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('with explicit EXTERNAL intent', () => {
|
|
39
|
+
it('should return EXTERNAL regardless of discovery', () => {
|
|
40
|
+
expect(resolveOwnership('external', false, false)).toBe('external');
|
|
41
|
+
expect(resolveOwnership('external', true, false)).toBe('external');
|
|
42
|
+
expect(resolveOwnership('external', false, true)).toBe('external');
|
|
43
|
+
expect(resolveOwnership('external', true, true)).toBe('external');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('with AUTO intent', () => {
|
|
48
|
+
it('should return STACK if resource is in stack (CRITICAL: must keep in template)', () => {
|
|
49
|
+
expect(resolveOwnership('auto', true, false)).toBe('stack');
|
|
50
|
+
expect(resolveOwnership('auto', true, true)).toBe('stack');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return EXTERNAL if found externally but not in stack', () => {
|
|
54
|
+
expect(resolveOwnership('auto', false, true)).toBe('external');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return STACK if not found anywhere (will create new)', () => {
|
|
58
|
+
expect(resolveOwnership('auto', false, false)).toBe('stack');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('edge cases', () => {
|
|
63
|
+
it('should throw on invalid intent', () => {
|
|
64
|
+
expect(() => resolveOwnership('invalid', false, false)).toThrow(
|
|
65
|
+
'Invalid ownership intent: invalid'
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle undefined intent as invalid', () => {
|
|
70
|
+
expect(() => resolveOwnership(undefined, false, false)).toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('real-world scenarios', () => {
|
|
75
|
+
it('scenario: fresh deploy, no resources exist', () => {
|
|
76
|
+
const ownership = resolveOwnership('auto', false, false);
|
|
77
|
+
expect(ownership).toBe('stack');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('scenario: redeploy existing stack, resource in stack', () => {
|
|
81
|
+
const ownership = resolveOwnership('auto', true, false);
|
|
82
|
+
expect(ownership).toBe('stack');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('scenario: resource exists in another stack/manually created', () => {
|
|
86
|
+
const ownership = resolveOwnership('auto', false, true);
|
|
87
|
+
expect(ownership).toBe('external');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('scenario: user explicitly wants to use external VPC', () => {
|
|
91
|
+
const ownership = resolveOwnership('external', false, false);
|
|
92
|
+
expect(ownership).toBe('external');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('scenario: user explicitly manages in stack even if external exists', () => {
|
|
96
|
+
const ownership = resolveOwnership('stack', false, true);
|
|
97
|
+
expect(ownership).toBe('stack');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.474.aa465e4.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@babel/eslint-parser": "^7.18.9",
|
|
12
12
|
"@babel/parser": "^7.25.3",
|
|
13
13
|
"@babel/traverse": "^7.25.3",
|
|
14
|
-
"@friggframework/schemas": "2.0.0--canary.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.
|
|
14
|
+
"@friggframework/schemas": "2.0.0--canary.474.aa465e4.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.474.aa465e4.0",
|
|
16
16
|
"@hapi/boom": "^10.0.1",
|
|
17
17
|
"@inquirer/prompts": "^5.3.8",
|
|
18
18
|
"axios": "^1.7.2",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"serverless-http": "^2.7.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
38
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
37
|
+
"@friggframework/eslint-config": "2.0.0--canary.474.aa465e4.0",
|
|
38
|
+
"@friggframework/prettier-config": "2.0.0--canary.474.aa465e4.0",
|
|
39
39
|
"aws-sdk-client-mock": "^4.1.0",
|
|
40
40
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
41
41
|
"jest": "^30.1.3",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "aa465e49207b1725bc36827e65efdd54ad879337"
|
|
74
74
|
}
|