serverless-bedrock-agentcore-plugin 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +476 -0
- package/package.json +96 -0
- package/src/compilers/browser.js +110 -0
- package/src/compilers/codeInterpreter.js +64 -0
- package/src/compilers/gateway.js +98 -0
- package/src/compilers/gatewayTarget.js +285 -0
- package/src/compilers/index.js +15 -0
- package/src/compilers/memory.js +168 -0
- package/src/compilers/runtime.js +234 -0
- package/src/compilers/runtimeEndpoint.js +37 -0
- package/src/compilers/workloadIdentity.js +36 -0
- package/src/docker/builder.js +236 -0
- package/src/iam/policies.js +591 -0
- package/src/index.js +1354 -0
- package/src/utils/index.js +9 -0
- package/src/utils/naming.js +79 -0
- package/src/utils/tags.js +56 -0
- package/src/validators/schema.js +250 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getResourceName, getLogicalId } = require('../utils/naming');
|
|
4
|
+
|
|
5
|
+
// Track if deprecation warning has been shown (module-level to show only once)
|
|
6
|
+
let deprecationWarningShown = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reset the deprecation warning flag (for testing purposes)
|
|
10
|
+
*/
|
|
11
|
+
function resetDeprecationWarning() {
|
|
12
|
+
deprecationWarningShown = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect if strategy is in legacy format
|
|
17
|
+
* Legacy format has flat structure with type/name at top level
|
|
18
|
+
* New format uses typed union like SemanticMemoryStrategy, etc.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} strategy - The strategy configuration
|
|
21
|
+
* @returns {boolean} True if legacy format
|
|
22
|
+
*/
|
|
23
|
+
function isLegacyFormat(strategy) {
|
|
24
|
+
// New format has one of these as top-level keys
|
|
25
|
+
const newFormatKeys = [
|
|
26
|
+
'SemanticMemoryStrategy',
|
|
27
|
+
'SummaryMemoryStrategy',
|
|
28
|
+
'UserPreferenceMemoryStrategy',
|
|
29
|
+
'CustomMemoryStrategy',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// If any new format key exists, it's new format
|
|
33
|
+
if (newFormatKeys.some((key) => strategy[key] !== undefined)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If it has 'type' property at top level, it's legacy format
|
|
38
|
+
return strategy.type !== undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert legacy strategy format to new typed union format
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} strategy - Legacy format strategy
|
|
45
|
+
* @returns {Object} New format strategy
|
|
46
|
+
*/
|
|
47
|
+
function convertLegacyStrategy(strategy) {
|
|
48
|
+
const baseConfig = {
|
|
49
|
+
Name: strategy.name,
|
|
50
|
+
...(strategy.description && { Description: strategy.description }),
|
|
51
|
+
...(strategy.namespaces && { Namespaces: strategy.namespaces }),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const type = (strategy.type || '').toLowerCase();
|
|
55
|
+
|
|
56
|
+
switch (type) {
|
|
57
|
+
case 'semantic':
|
|
58
|
+
return {
|
|
59
|
+
SemanticMemoryStrategy: {
|
|
60
|
+
...baseConfig,
|
|
61
|
+
// Note: Managed strategies don't require explicit Type or model configuration
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
case 'summary':
|
|
66
|
+
case 'summarization':
|
|
67
|
+
return {
|
|
68
|
+
SummaryMemoryStrategy: {
|
|
69
|
+
...baseConfig,
|
|
70
|
+
// Note: Managed strategies don't require explicit Type
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
case 'userpreference':
|
|
75
|
+
case 'user_preference':
|
|
76
|
+
return {
|
|
77
|
+
UserPreferenceMemoryStrategy: {
|
|
78
|
+
...baseConfig,
|
|
79
|
+
// Note: Managed strategies don't require explicit Type
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
case 'custom':
|
|
84
|
+
return {
|
|
85
|
+
CustomMemoryStrategy: {
|
|
86
|
+
...baseConfig,
|
|
87
|
+
...(strategy.configuration && { Configuration: strategy.configuration }),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`Unknown memory strategy type: ${type}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build memory strategies configuration
|
|
98
|
+
* Supports both legacy flat format and new typed union format
|
|
99
|
+
*
|
|
100
|
+
* @param {Array} strategies - The strategies configuration from serverless.yml
|
|
101
|
+
* @returns {Array|null} CloudFormation memory strategies or null
|
|
102
|
+
*/
|
|
103
|
+
function buildMemoryStrategies(strategies) {
|
|
104
|
+
if (!strategies || strategies.length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return strategies.map((strategy) => {
|
|
109
|
+
if (isLegacyFormat(strategy)) {
|
|
110
|
+
// Emit deprecation warning once per deployment
|
|
111
|
+
if (!deprecationWarningShown) {
|
|
112
|
+
console.warn(
|
|
113
|
+
'\x1b[33m%s\x1b[0m', // Yellow color
|
|
114
|
+
'DEPRECATION WARNING: Memory strategy format has changed to typed union structure. ' +
|
|
115
|
+
'Please update your configuration. See documentation for migration guide.'
|
|
116
|
+
);
|
|
117
|
+
deprecationWarningShown = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return convertLegacyStrategy(strategy);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Already in new format - pass through
|
|
124
|
+
return strategy;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compile a Memory resource to CloudFormation
|
|
130
|
+
*
|
|
131
|
+
* @param {string} name - The agent name
|
|
132
|
+
* @param {Object} config - The memory configuration
|
|
133
|
+
* @param {Object} context - The compilation context
|
|
134
|
+
* @param {Object} tags - The merged tags
|
|
135
|
+
* @returns {Object} CloudFormation resource definition
|
|
136
|
+
*/
|
|
137
|
+
function compileMemory(name, config, context, tags) {
|
|
138
|
+
const { serviceName, stage } = context;
|
|
139
|
+
const resourceName = getResourceName(serviceName, name, stage);
|
|
140
|
+
const roleLogicalId = `${getLogicalId(name, 'Memory')}Role`;
|
|
141
|
+
|
|
142
|
+
const strategies = buildMemoryStrategies(config.strategies);
|
|
143
|
+
|
|
144
|
+
// Default expiry duration to 30 days if not specified
|
|
145
|
+
const eventExpiryDuration = config.eventExpiryDuration || 30;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
Type: 'AWS::BedrockAgentCore::Memory',
|
|
149
|
+
Properties: {
|
|
150
|
+
Name: resourceName,
|
|
151
|
+
EventExpiryDuration: eventExpiryDuration,
|
|
152
|
+
...(config.description && { Description: config.description }),
|
|
153
|
+
...(config.encryptionKeyArn && { EncryptionKeyArn: config.encryptionKeyArn }),
|
|
154
|
+
...(!config.roleArn && { MemoryExecutionRoleArn: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] } }),
|
|
155
|
+
...(config.roleArn && { MemoryExecutionRoleArn: config.roleArn }),
|
|
156
|
+
...(strategies && { MemoryStrategies: strategies }),
|
|
157
|
+
...(Object.keys(tags).length > 0 && { Tags: tags }),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
compileMemory,
|
|
164
|
+
buildMemoryStrategies,
|
|
165
|
+
isLegacyFormat,
|
|
166
|
+
convertLegacyStrategy,
|
|
167
|
+
resetDeprecationWarning,
|
|
168
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getResourceName, getLogicalId } = require('../utils/naming');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the artifact configuration for the runtime
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} artifact - The artifact configuration from serverless.yml
|
|
9
|
+
* @returns {Object} CloudFormation artifact configuration
|
|
10
|
+
*/
|
|
11
|
+
function buildArtifact(artifact) {
|
|
12
|
+
if (artifact.containerImage) {
|
|
13
|
+
return {
|
|
14
|
+
ContainerConfiguration: {
|
|
15
|
+
ContainerUri: artifact.containerImage,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (artifact.s3) {
|
|
21
|
+
return {
|
|
22
|
+
S3Configuration: {
|
|
23
|
+
S3BucketName: artifact.s3.bucket,
|
|
24
|
+
S3ObjectKey: artifact.s3.key,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error('Artifact must specify either containerImage or s3 configuration');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build network configuration for the runtime
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} network - The network configuration from serverless.yml
|
|
36
|
+
* @returns {Object} CloudFormation network configuration
|
|
37
|
+
*/
|
|
38
|
+
function buildNetworkConfiguration(network = {}) {
|
|
39
|
+
const networkMode = network.networkMode || 'PUBLIC';
|
|
40
|
+
|
|
41
|
+
const config = {
|
|
42
|
+
NetworkMode: networkMode,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Add VPC configuration if VPC mode
|
|
46
|
+
if (networkMode === 'VPC' && network.vpcConfig) {
|
|
47
|
+
config.VpcConfiguration = {
|
|
48
|
+
...(network.vpcConfig.subnetIds && { SubnetIds: network.vpcConfig.subnetIds }),
|
|
49
|
+
...(network.vpcConfig.securityGroupIds && {
|
|
50
|
+
SecurityGroupIds: network.vpcConfig.securityGroupIds,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build authorizer configuration for the runtime
|
|
60
|
+
* Supports NONE, AWS_IAM, and CustomJWTAuthorizer
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} authorizer - The authorizer configuration from serverless.yml
|
|
63
|
+
* @returns {Object|null} CloudFormation authorizer configuration or null
|
|
64
|
+
*/
|
|
65
|
+
function buildAuthorizerConfiguration(authorizer) {
|
|
66
|
+
if (!authorizer) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle CustomJWTAuthorizer - uses OIDC discovery URL
|
|
71
|
+
if (authorizer.customJwtAuthorizer) {
|
|
72
|
+
const jwtConfig = authorizer.customJwtAuthorizer;
|
|
73
|
+
|
|
74
|
+
// DiscoveryUrl is required for CustomJWTAuthorizer
|
|
75
|
+
if (!jwtConfig.discoveryUrl) {
|
|
76
|
+
throw new Error('CustomJWTAuthorizer requires discoveryUrl');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
CustomJWTAuthorizer: {
|
|
81
|
+
DiscoveryUrl: jwtConfig.discoveryUrl,
|
|
82
|
+
...(jwtConfig.allowedAudience && { AllowedAudience: jwtConfig.allowedAudience }),
|
|
83
|
+
...(jwtConfig.allowedClients && { AllowedClients: jwtConfig.allowedClients }),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle legacy format with type field
|
|
89
|
+
if (authorizer.type === 'CUSTOM_JWT' && authorizer.jwtConfiguration) {
|
|
90
|
+
// Convert legacy format to new format
|
|
91
|
+
const discoveryUrl =
|
|
92
|
+
authorizer.jwtConfiguration.discoveryUrl || authorizer.jwtConfiguration.issuer;
|
|
93
|
+
|
|
94
|
+
if (!discoveryUrl) {
|
|
95
|
+
throw new Error('CustomJWTAuthorizer requires discoveryUrl or issuer');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
CustomJWTAuthorizer: {
|
|
100
|
+
DiscoveryUrl: discoveryUrl,
|
|
101
|
+
...(authorizer.jwtConfiguration.allowedAudience && {
|
|
102
|
+
AllowedAudience: authorizer.jwtConfiguration.allowedAudience,
|
|
103
|
+
}),
|
|
104
|
+
...(authorizer.jwtConfiguration.allowedClients && {
|
|
105
|
+
AllowedClients: authorizer.jwtConfiguration.allowedClients,
|
|
106
|
+
}),
|
|
107
|
+
// Legacy support: audience -> allowedAudience
|
|
108
|
+
...(authorizer.jwtConfiguration.audience && {
|
|
109
|
+
AllowedAudience: Array.isArray(authorizer.jwtConfiguration.audience)
|
|
110
|
+
? authorizer.jwtConfiguration.audience
|
|
111
|
+
: [authorizer.jwtConfiguration.audience],
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Return null for NONE or AWS_IAM (handled at runtime level, not in AuthorizerConfiguration)
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build lifecycle configuration for the runtime
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} lifecycle - The lifecycle configuration from serverless.yml
|
|
125
|
+
* @returns {Object|null} CloudFormation lifecycle configuration or null
|
|
126
|
+
*/
|
|
127
|
+
function buildLifecycleConfiguration(lifecycle) {
|
|
128
|
+
if (!lifecycle) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...(lifecycle.idleTimeout !== undefined && { IdleTimeoutSeconds: lifecycle.idleTimeout }),
|
|
134
|
+
...(lifecycle.maxConcurrency !== undefined && { MaxConcurrency: lifecycle.maxConcurrency }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build protocol configuration for the runtime
|
|
140
|
+
* ProtocolConfiguration is a string enum: HTTP, MCP, or A2A
|
|
141
|
+
*
|
|
142
|
+
* @param {string} protocol - The protocol type (HTTP, MCP, A2A)
|
|
143
|
+
* @returns {string|null} Protocol string or null
|
|
144
|
+
*/
|
|
145
|
+
function buildProtocolConfiguration(protocol) {
|
|
146
|
+
if (!protocol) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ProtocolConfiguration is just the protocol string, not an object
|
|
151
|
+
return protocol;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build environment variables for the runtime
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} environment - Environment variables object
|
|
158
|
+
* @returns {Object|null} Environment variables or null
|
|
159
|
+
*/
|
|
160
|
+
function buildEnvironmentVariables(environment) {
|
|
161
|
+
if (!environment || Object.keys(environment).length === 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return environment;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build request header configuration for the runtime
|
|
170
|
+
* Allows specific headers to be forwarded to the agent
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} requestHeaders - The request headers configuration from serverless.yml
|
|
173
|
+
* @returns {Object|null} CloudFormation RequestHeaderConfiguration or null
|
|
174
|
+
*/
|
|
175
|
+
function buildRequestHeaderConfiguration(requestHeaders) {
|
|
176
|
+
if (!requestHeaders || !requestHeaders.allowlist || requestHeaders.allowlist.length === 0) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
RequestHeaderAllowlist: requestHeaders.allowlist,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compile a Runtime resource to CloudFormation
|
|
187
|
+
*
|
|
188
|
+
* @param {string} name - The agent name
|
|
189
|
+
* @param {Object} config - The runtime configuration
|
|
190
|
+
* @param {Object} context - The compilation context
|
|
191
|
+
* @param {Object} tags - The merged tags
|
|
192
|
+
* @returns {Object} CloudFormation resource definition
|
|
193
|
+
*/
|
|
194
|
+
function compileRuntime(name, config, context, tags) {
|
|
195
|
+
const { serviceName, stage } = context;
|
|
196
|
+
const resourceName = getResourceName(serviceName, name, stage);
|
|
197
|
+
const roleLogicalId = `${getLogicalId(name, 'Runtime')}Role`;
|
|
198
|
+
|
|
199
|
+
const artifact = buildArtifact(config.artifact);
|
|
200
|
+
const networkConfig = buildNetworkConfiguration(config.network);
|
|
201
|
+
const authorizerConfig = buildAuthorizerConfiguration(config.authorizer);
|
|
202
|
+
const lifecycleConfig = buildLifecycleConfiguration(config.lifecycle);
|
|
203
|
+
const protocolConfig = buildProtocolConfiguration(config.protocol);
|
|
204
|
+
const envVars = buildEnvironmentVariables(config.environment);
|
|
205
|
+
const requestHeaderConfig = buildRequestHeaderConfiguration(config.requestHeaders);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
Type: 'AWS::BedrockAgentCore::Runtime',
|
|
209
|
+
Properties: {
|
|
210
|
+
AgentRuntimeName: resourceName,
|
|
211
|
+
AgentRuntimeArtifact: artifact,
|
|
212
|
+
NetworkConfiguration: networkConfig,
|
|
213
|
+
RoleArn: config.roleArn || { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
|
|
214
|
+
...(config.description && { Description: config.description }),
|
|
215
|
+
...(authorizerConfig && { AuthorizerConfiguration: authorizerConfig }),
|
|
216
|
+
...(lifecycleConfig && { LifecycleConfiguration: lifecycleConfig }),
|
|
217
|
+
...(protocolConfig && { ProtocolConfiguration: protocolConfig }),
|
|
218
|
+
...(envVars && { EnvironmentVariables: envVars }),
|
|
219
|
+
...(requestHeaderConfig && { RequestHeaderConfiguration: requestHeaderConfig }),
|
|
220
|
+
...(Object.keys(tags).length > 0 && { Tags: tags }),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
compileRuntime,
|
|
227
|
+
buildArtifact,
|
|
228
|
+
buildNetworkConfiguration,
|
|
229
|
+
buildAuthorizerConfiguration,
|
|
230
|
+
buildLifecycleConfiguration,
|
|
231
|
+
buildProtocolConfiguration,
|
|
232
|
+
buildEnvironmentVariables,
|
|
233
|
+
buildRequestHeaderConfiguration,
|
|
234
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getResourceName } = require('../utils/naming');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compile a RuntimeEndpoint resource to CloudFormation
|
|
7
|
+
*
|
|
8
|
+
* @param {string} agentName - The parent agent name
|
|
9
|
+
* @param {string} endpointName - The endpoint name
|
|
10
|
+
* @param {Object} config - The endpoint configuration
|
|
11
|
+
* @param {string} runtimeLogicalId - The logical ID of the parent Runtime resource
|
|
12
|
+
* @param {Object} context - The compilation context
|
|
13
|
+
* @param {Object} tags - The merged tags
|
|
14
|
+
* @returns {Object} CloudFormation resource definition
|
|
15
|
+
*/
|
|
16
|
+
function compileRuntimeEndpoint(agentName, endpointName, config, runtimeLogicalId, context, tags) {
|
|
17
|
+
const { serviceName, stage } = context;
|
|
18
|
+
|
|
19
|
+
// Generate endpoint name: {service}_{agent}_{endpoint}_{stage}
|
|
20
|
+
const resourceName = getResourceName(serviceName, `${agentName}_${endpointName}`, stage);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
Type: 'AWS::BedrockAgentCore::RuntimeEndpoint',
|
|
24
|
+
DependsOn: [runtimeLogicalId],
|
|
25
|
+
Properties: {
|
|
26
|
+
Name: resourceName,
|
|
27
|
+
AgentRuntimeId: { 'Fn::GetAtt': [runtimeLogicalId, 'AgentRuntimeId'] },
|
|
28
|
+
...(config.version && { AgentRuntimeVersion: config.version }),
|
|
29
|
+
...(config.description && { Description: config.description }),
|
|
30
|
+
...(Object.keys(tags).length > 0 && { Tags: tags }),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
compileRuntimeEndpoint,
|
|
37
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getResourceName } = require('../utils/naming');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compile a WorkloadIdentity resource to CloudFormation
|
|
7
|
+
*
|
|
8
|
+
* @param {string} name - The workload identity name
|
|
9
|
+
* @param {Object} config - The workload identity configuration
|
|
10
|
+
* @param {Object} context - The compilation context
|
|
11
|
+
* @param {Object} tags - The merged tags
|
|
12
|
+
* @returns {Object} CloudFormation resource definition
|
|
13
|
+
*/
|
|
14
|
+
function compileWorkloadIdentity(name, config, context, tags) {
|
|
15
|
+
const { serviceName, stage } = context;
|
|
16
|
+
// WorkloadIdentity name pattern: [A-Za-z0-9_.-]+
|
|
17
|
+
// Replace underscores with hyphens to match the pattern better
|
|
18
|
+
const resourceName = getResourceName(serviceName, name, stage).replace(/_/g, '-');
|
|
19
|
+
|
|
20
|
+
// Convert tags object to array format for WorkloadIdentity
|
|
21
|
+
// WorkloadIdentity uses Array[Tag] format instead of Object format
|
|
22
|
+
const tagArray = Object.entries(tags).map(([Key, Value]) => ({ Key, Value }));
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
Type: 'AWS::BedrockAgentCore::WorkloadIdentity',
|
|
26
|
+
Properties: {
|
|
27
|
+
Name: resourceName,
|
|
28
|
+
...(config.oauth2ReturnUrls && { AllowedResourceOauth2ReturnUrls: config.oauth2ReturnUrls }),
|
|
29
|
+
...(tagArray.length > 0 && { Tags: tagArray }),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
compileWorkloadIdentity,
|
|
36
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Execute a shell command synchronously
|
|
8
|
+
*/
|
|
9
|
+
function execCommand(command, options = {}) {
|
|
10
|
+
try {
|
|
11
|
+
return execSync(command, {
|
|
12
|
+
encoding: 'utf-8',
|
|
13
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
14
|
+
...options,
|
|
15
|
+
});
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (options.ignoreError) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Docker image builder for AgentCore runtimes
|
|
26
|
+
* Follows similar patterns to Serverless Framework's ECR image handling
|
|
27
|
+
*/
|
|
28
|
+
class DockerBuilder {
|
|
29
|
+
constructor(serverless, log, progress) {
|
|
30
|
+
this.serverless = serverless;
|
|
31
|
+
this.log = log;
|
|
32
|
+
this.progress = progress;
|
|
33
|
+
this.provider = serverless.getProvider('aws');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if Docker is available
|
|
38
|
+
*/
|
|
39
|
+
checkDocker() {
|
|
40
|
+
try {
|
|
41
|
+
execCommand('docker --version', { silent: true });
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get AWS account ID
|
|
51
|
+
*/
|
|
52
|
+
async getAccountId() {
|
|
53
|
+
return this.provider.getAccountId();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the region
|
|
58
|
+
*/
|
|
59
|
+
getRegion() {
|
|
60
|
+
return this.provider.getRegion();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Authenticate with ECR
|
|
65
|
+
*/
|
|
66
|
+
async ecrLogin(accountId, region) {
|
|
67
|
+
this.log.info('Authenticating with ECR...');
|
|
68
|
+
|
|
69
|
+
const ecrUri = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Get ECR login password and pipe to docker login
|
|
73
|
+
const password = execCommand(`aws ecr get-login-password --region ${region}`, {
|
|
74
|
+
silent: true,
|
|
75
|
+
}).trim();
|
|
76
|
+
|
|
77
|
+
execCommand(`docker login --username AWS --password-stdin ${ecrUri}`, {
|
|
78
|
+
input: password,
|
|
79
|
+
silent: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.log.info('ECR authentication successful');
|
|
83
|
+
|
|
84
|
+
return ecrUri;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`ECR login failed: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ensure ECR repository exists, create if not
|
|
92
|
+
*/
|
|
93
|
+
async ensureRepository(repositoryName, region) {
|
|
94
|
+
this.log.info(`Checking ECR repository: ${repositoryName}`);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
execCommand(
|
|
98
|
+
`aws ecr describe-repositories --repository-names ${repositoryName} --region ${region}`,
|
|
99
|
+
{ silent: true }
|
|
100
|
+
);
|
|
101
|
+
this.log.info('Repository exists');
|
|
102
|
+
} catch {
|
|
103
|
+
this.log.info('Creating ECR repository...');
|
|
104
|
+
execCommand(
|
|
105
|
+
`aws ecr create-repository --repository-name ${repositoryName} --region ${region}`,
|
|
106
|
+
{ silent: true }
|
|
107
|
+
);
|
|
108
|
+
this.log.info('Repository created');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build Docker image
|
|
114
|
+
*/
|
|
115
|
+
async buildImage(imageTag, dockerConfig, servicePath) {
|
|
116
|
+
const dockerfile = dockerConfig.file || 'Dockerfile';
|
|
117
|
+
const context = dockerConfig.path || '.';
|
|
118
|
+
// Bedrock AgentCore requires arm64 architecture
|
|
119
|
+
const platform = dockerConfig.platform || 'linux/arm64';
|
|
120
|
+
|
|
121
|
+
const dockerfilePath = path.resolve(servicePath, context, dockerfile);
|
|
122
|
+
const contextPath = path.resolve(servicePath, context);
|
|
123
|
+
|
|
124
|
+
this.log.info(`Building Docker image: ${imageTag}`);
|
|
125
|
+
this.log.info(` Dockerfile: ${dockerfilePath}`);
|
|
126
|
+
this.log.info(` Context: ${contextPath}`);
|
|
127
|
+
this.log.info(` Platform: ${platform}`);
|
|
128
|
+
|
|
129
|
+
// Build the docker command
|
|
130
|
+
let buildCmd = `docker build --platform ${platform} -t ${imageTag}`;
|
|
131
|
+
|
|
132
|
+
// Add build args if specified
|
|
133
|
+
if (dockerConfig.buildArgs) {
|
|
134
|
+
for (const [key, value] of Object.entries(dockerConfig.buildArgs)) {
|
|
135
|
+
buildCmd += ` --build-arg ${key}="${value}"`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add cache from if specified
|
|
140
|
+
if (dockerConfig.cacheFrom) {
|
|
141
|
+
for (const cache of dockerConfig.cacheFrom) {
|
|
142
|
+
buildCmd += ` --cache-from ${cache}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add any additional build options
|
|
147
|
+
if (dockerConfig.buildOptions) {
|
|
148
|
+
buildCmd += ` ${dockerConfig.buildOptions.join(' ')}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
buildCmd += ` -f ${dockerfilePath} ${contextPath}`;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
execCommand(buildCmd);
|
|
155
|
+
this.log.info('Docker build complete');
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(`Docker build failed: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Tag and push image to ECR
|
|
163
|
+
*/
|
|
164
|
+
async pushImage(localTag, ecrUri, repositoryName, tag) {
|
|
165
|
+
const ecrImage = `${ecrUri}/${repositoryName}:${tag}`;
|
|
166
|
+
|
|
167
|
+
this.log.info(`Tagging image: ${ecrImage}`);
|
|
168
|
+
execCommand(`docker tag ${localTag} ${ecrImage}`);
|
|
169
|
+
|
|
170
|
+
this.log.info(`Pushing image to ECR...`);
|
|
171
|
+
execCommand(`docker push ${ecrImage}`);
|
|
172
|
+
|
|
173
|
+
this.log.info('Push complete');
|
|
174
|
+
|
|
175
|
+
return ecrImage;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build and push image for a runtime agent
|
|
180
|
+
* Returns the ECR image URI to use in CloudFormation
|
|
181
|
+
*/
|
|
182
|
+
async buildAndPushForRuntime(agentName, imageConfig, context) {
|
|
183
|
+
const { serviceName, stage, region } = context;
|
|
184
|
+
const accountId = await this.getAccountId();
|
|
185
|
+
const servicePath = this.serverless.serviceDir;
|
|
186
|
+
|
|
187
|
+
// Determine repository name
|
|
188
|
+
const repositoryName = imageConfig.repository || `${serviceName}-${agentName}`;
|
|
189
|
+
|
|
190
|
+
// Determine tag (use stage or specified tag)
|
|
191
|
+
const tag = imageConfig.tag || stage;
|
|
192
|
+
|
|
193
|
+
// Local image tag
|
|
194
|
+
const localTag = `${repositoryName}:${tag}`;
|
|
195
|
+
|
|
196
|
+
// Ensure repository exists
|
|
197
|
+
await this.ensureRepository(repositoryName, region);
|
|
198
|
+
|
|
199
|
+
// Login to ECR
|
|
200
|
+
const ecrUri = await this.ecrLogin(accountId, region);
|
|
201
|
+
|
|
202
|
+
// Build image
|
|
203
|
+
await this.buildImage(localTag, imageConfig, servicePath);
|
|
204
|
+
|
|
205
|
+
// Push to ECR
|
|
206
|
+
const ecrImage = await this.pushImage(localTag, ecrUri, repositoryName, tag);
|
|
207
|
+
|
|
208
|
+
return ecrImage;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Process all images defined in provider.ecr.images
|
|
213
|
+
* Returns a map of image names to ECR URIs
|
|
214
|
+
*/
|
|
215
|
+
async processImages(imagesConfig, context) {
|
|
216
|
+
const imageUris = {};
|
|
217
|
+
|
|
218
|
+
for (const [imageName, imageConfig] of Object.entries(imagesConfig)) {
|
|
219
|
+
// If URI is already specified, use it directly
|
|
220
|
+
if (imageConfig.uri) {
|
|
221
|
+
imageUris[imageName] = imageConfig.uri;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Otherwise, build and push
|
|
226
|
+
if (imageConfig.path) {
|
|
227
|
+
const uri = await this.buildAndPushForRuntime(imageName, imageConfig, context);
|
|
228
|
+
imageUris[imageName] = uri;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return imageUris;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { DockerBuilder };
|