@aws/nx-plugin 1.1.1 → 1.3.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/README.md +31 -29
- package/generators.json +16 -0
- package/migrations.json +6 -1
- package/package.json +1 -1
- package/src/agentcore-harness/generator.d.ts +1 -1
- package/src/greengrass/component/common.d.ts +60 -0
- package/src/greengrass/component/common.js +191 -0
- package/src/greengrass/component/common.js.map +1 -0
- package/src/greengrass/component/dependencies.d.ts +101 -0
- package/src/greengrass/component/dependencies.js +37 -0
- package/src/greengrass/component/dependencies.js.map +1 -0
- package/src/greengrass/component/generator.d.ts +174 -0
- package/src/greengrass/component/generator.js +32 -0
- package/src/greengrass/component/generator.js.map +1 -0
- package/src/greengrass/component/metadata.d.ts +2 -0
- package/src/greengrass/component/metadata.js +14 -0
- package/src/greengrass/component/metadata.js.map +1 -0
- package/src/greengrass/component/python/__snapshots__/generator.spec.ts.snap +450 -0
- package/src/greengrass/component/python/files/component/main.py.template +60 -0
- package/src/greengrass/component/python/files/component/recipe.yaml.template +30 -0
- package/src/greengrass/component/python/files/gdk/gdk-config.json.template +13 -0
- package/src/greengrass/component/python/files/tests/test___componentSnakeCase__.py.template +37 -0
- package/src/greengrass/component/python/generator.d.ts +15 -0
- package/src/greengrass/component/python/generator.js +192 -0
- package/src/greengrass/component/python/generator.js.map +1 -0
- package/src/greengrass/component/schema.d.js +6 -0
- package/src/greengrass/component/schema.d.js.map +1 -0
- package/src/greengrass/component/schema.d.ts +18 -0
- package/src/greengrass/component/schema.json +82 -0
- package/src/greengrass/component/typescript/__snapshots__/generator.spec.ts.snap +275 -0
- package/src/greengrass/component/typescript/files/component/recipe.yaml.template +41 -0
- package/src/greengrass/component/typescript/files/entrypoint/main.ts.template +58 -0
- package/src/greengrass/component/typescript/files/gdk/gdk-config.json.template +13 -0
- package/src/greengrass/component/typescript/files/scripts/vendor-crt-addon.ts.template +115 -0
- package/src/greengrass/component/typescript/generator.d.ts +16 -0
- package/src/greengrass/component/typescript/generator.js +194 -0
- package/src/greengrass/component/typescript/generator.js.map +1 -0
- package/src/infra/app/generator.d.ts +1 -1
- package/src/init/generator.d.ts +1 -1
- package/src/internal/test-matrix/generator.js +48 -1
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/license/dependency-check/pre-approved.js +2 -1
- package/src/license/dependency-check/pre-approved.js.map +1 -1
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/metadata.json +3 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
- package/src/open-api/py-client/generator.d.ts +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +4 -0
- package/src/preset/generator.d.ts +1 -1
- package/src/py/agent/a2a-connection/generator.d.ts +1 -1
- package/src/py/agent/gateway-connection/generator.d.ts +1 -1
- package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
- package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
- package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
- package/src/py/agent/guardrails/generator.d.ts +10 -0
- package/src/py/agent/guardrails/generator.js +175 -0
- package/src/py/agent/guardrails/generator.js.map +1 -0
- package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
- package/src/py/agent/guardrails/schema.d.js +6 -0
- package/src/py/agent/guardrails/schema.d.js.map +1 -0
- package/src/py/agent/guardrails/schema.d.ts +9 -0
- package/src/py/agent/guardrails/schema.json +25 -0
- package/src/py/agent/mcp-connection/generator.d.ts +1 -1
- package/src/py/agent/react-connection/generator.d.ts +1 -1
- package/src/py/fast-api/react/generator.d.ts +1 -1
- package/src/py/project/generator.js +2 -0
- package/src/py/project/generator.js.map +1 -1
- package/src/sdk/greengrass.d.ts +6 -0
- package/src/sdk/greengrass.js +6 -0
- package/src/sdk/greengrass.js.map +1 -0
- package/src/sdk/py.d.ts +2 -0
- package/src/sdk/py.js +1 -0
- package/src/sdk/py.js.map +1 -1
- package/src/smithy/project/generator.d.ts +1 -1
- package/src/smithy/react-connection/generator.d.ts +1 -1
- package/src/terraform/project/generator.d.ts +1 -1
- package/src/trpc/react/generator.d.ts +1 -1
- package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
- package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
- package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +54 -3
- package/src/ts/agent/guardrails/generator.js +2 -1
- package/src/ts/agent/guardrails/generator.js.map +1 -1
- package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
- package/src/ts/agent/react-connection/generator.d.ts +1 -1
- package/src/ts/astro-docs/generator.d.ts +1 -1
- package/src/ts/dcr-proxy/generator.d.ts +1 -1
- package/src/ts/dynamodb/generator.d.ts +1 -1
- package/src/ts/lambda-function/generator.d.ts +1 -1
- package/src/ts/lib/generator.d.ts +1 -1
- package/src/ts/nx-generator/generator.d.ts +1 -1
- package/src/ts/nx-migration/generator.d.ts +1 -1
- package/src/ts/nx-plugin/generator.d.ts +1 -1
- package/src/ts/rdb/generator.d.ts +1 -1
- package/src/ts/react-website/agui/generator.d.ts +1 -1
- package/src/utils/__snapshots__/shared-greengrass-scripts.spec.ts.snap +1279 -0
- package/src/utils/files/common/scripts/src/greengrass/build-artifact.ts.template +236 -0
- package/src/utils/files/common/scripts/src/greengrass/component-logs.ts.template +48 -0
- package/src/utils/files/common/scripts/src/greengrass/deploy-local.ts.template +497 -0
- package/src/utils/files/common/scripts/src/greengrass/recipe-utils.ts.template +356 -0
- package/src/utils/files/common/scripts/src/greengrass/zip-writer.ts.template +121 -0
- package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +3 -3
- package/src/utils/files/terraform/src/metrics/metrics.tf.template +1 -1
- package/src/utils/greengrass/constants.d.ts +58 -0
- package/src/utils/greengrass/constants.js +63 -0
- package/src/utils/greengrass/constants.js.map +1 -0
- package/src/utils/greengrass/naming.d.ts +46 -0
- package/src/utils/greengrass/naming.js +67 -0
- package/src/utils/greengrass/naming.js.map +1 -0
- package/src/utils/greengrass/recipe.d.ts +143 -0
- package/src/utils/greengrass/recipe.js +119 -0
- package/src/utils/greengrass/recipe.js.map +1 -0
- package/src/utils/greengrass-constructs/files/cdk/app/greengrass-component/__componentDirName__.ts.template +42 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/artifact-bucket.ts.template +82 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/component-version.ts.template +370 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/deployment.ts.template +276 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/index.ts.template +5 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/next-version.ts.template +119 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/recipe.ts.template +90 -0
- package/src/utils/greengrass-constructs/files/next-version/__nextVersionFileName__.template +195 -0
- package/src/utils/greengrass-constructs/files/terraform/app/greengrass-component/__componentDirName__/__componentDirName__.tf.template +26 -0
- package/src/utils/greengrass-constructs/files/terraform/core/greengrass/artifact-bucket/main.tf.template +204 -0
- package/src/utils/greengrass-constructs/files/terraform/core/greengrass/component-version/main.tf.template +156 -0
- package/src/utils/greengrass-constructs/files/terraform/core/greengrass/deployment/main.tf.template +186 -0
- package/src/utils/greengrass-constructs/greengrass-constructs.d.ts +57 -0
- package/src/utils/greengrass-constructs/greengrass-constructs.js +154 -0
- package/src/utils/greengrass-constructs/greengrass-constructs.js.map +1 -0
- package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
- package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
- package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
- package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
- package/src/utils/nx.d.ts +15 -0
- package/src/utils/nx.js +32 -0
- package/src/utils/nx.js.map +1 -1
- package/src/utils/shared-greengrass-scripts.d.ts +21 -0
- package/src/utils/shared-greengrass-scripts.js +43 -0
- package/src/utils/shared-greengrass-scripts.js.map +1 -0
- package/src/utils/test/fake-aws/fake_aws.py +205 -0
- package/src/utils/test/python-dependencies.d.ts +6 -0
- package/src/utils/test/python-dependencies.js +19 -0
- package/src/utils/test/python-dependencies.js.map +1 -1
- package/src/utils/test/warm-python-cache.js +5 -1
- package/src/utils/test/warm-python-cache.js.map +1 -1
- package/src/utils/versions.d.ts +7 -0
- package/src/utils/versions.js +7 -0
- package/src/utils/versions.js.map +1 -1
- /package/src/{ts/agent/guardrails/files/agent → utils/guardrail-constructs/files/policy}/guardrail.json.template +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import * as url from 'node:url';
|
|
3
|
+
import { CustomResource, Duration, Stack } from 'aws-cdk-lib';
|
|
4
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
5
|
+
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
|
|
6
|
+
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
7
|
+
|
|
8
|
+
const GreengrassNextVersionProviderKey = '__GreengrassNextVersionProvider__';
|
|
9
|
+
|
|
10
|
+
export interface GreengrassNextVersionProps {
|
|
11
|
+
/** Name of the Greengrass component whose next version is resolved. */
|
|
12
|
+
readonly componentName: string;
|
|
13
|
+
/** Part of the highest published strict M.m.p version to increment. */
|
|
14
|
+
readonly strategy: 'patch' | 'minor' | 'major';
|
|
15
|
+
/** Opaque digest of the submitted recipe and built artifact content. */
|
|
16
|
+
readonly contentSha256: string;
|
|
17
|
+
/** Artifact bucket name, which can be a deploy-time CloudFormation token. */
|
|
18
|
+
readonly bucketName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Share one resolver Lambda and provider across a stack's components.
|
|
23
|
+
*/
|
|
24
|
+
class GreengrassNextVersionProvider extends Construct {
|
|
25
|
+
static ensure(scope: Construct): GreengrassNextVersionProvider {
|
|
26
|
+
const stack = Stack.of(scope);
|
|
27
|
+
return (
|
|
28
|
+
(stack.node.tryFindChild(
|
|
29
|
+
GreengrassNextVersionProviderKey,
|
|
30
|
+
) as GreengrassNextVersionProvider) ??
|
|
31
|
+
new GreengrassNextVersionProvider(
|
|
32
|
+
stack,
|
|
33
|
+
GreengrassNextVersionProviderKey,
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public readonly serviceToken: string;
|
|
39
|
+
private readonly onEvent: Function;
|
|
40
|
+
private readonly componentNames = new Set<string>();
|
|
41
|
+
|
|
42
|
+
public allowComponent(componentName: string): void {
|
|
43
|
+
if (this.componentNames.has(componentName)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.componentNames.add(componentName);
|
|
47
|
+
const stack = Stack.of(this);
|
|
48
|
+
const componentArn = `arn:${stack.partition}:greengrass:${stack.region}:${stack.account}:components:${componentName}`;
|
|
49
|
+
this.onEvent.addToRolePolicy(
|
|
50
|
+
new PolicyStatement({
|
|
51
|
+
actions: ['greengrass:ListComponentVersions'],
|
|
52
|
+
resources: [componentArn],
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
this.onEvent.addToRolePolicy(
|
|
56
|
+
new PolicyStatement({
|
|
57
|
+
actions: ['greengrass:DescribeComponent'],
|
|
58
|
+
resources: [`${componentArn}:versions:*`],
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private constructor(scope: Construct, id: string) {
|
|
64
|
+
super(scope, id);
|
|
65
|
+
|
|
66
|
+
this.onEvent = new Function(this, 'OnEvent', {
|
|
67
|
+
runtime: <%- nodeRuntime %>,
|
|
68
|
+
handler: 'index.handler',
|
|
69
|
+
timeout: Duration.minutes(2),
|
|
70
|
+
code: Code.fromAsset(
|
|
71
|
+
url.fileURLToPath(
|
|
72
|
+
new URL('./next-version', <% if (esm) { %>import.meta.url<% } else { %>require('url').pathToFileURL(__filename).href<% } %>),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
});
|
|
76
|
+
const provider = new Provider(this, 'Provider', {
|
|
77
|
+
onEventHandler: this.onEvent,
|
|
78
|
+
});
|
|
79
|
+
this.serviceToken = provider.serviceToken;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the version and content identity when its properties change.
|
|
85
|
+
* Deletion leaves the resolved component version in the registry.
|
|
86
|
+
*/
|
|
87
|
+
export class GreengrassNextVersion extends Construct {
|
|
88
|
+
/** The deploy-time version resolved from the Greengrass registry. */
|
|
89
|
+
public readonly version: string;
|
|
90
|
+
|
|
91
|
+
/** Bucket-scoped content identity returned by the shared resolver. */
|
|
92
|
+
public readonly contentKey: string;
|
|
93
|
+
|
|
94
|
+
constructor(
|
|
95
|
+
scope: Construct,
|
|
96
|
+
id: string,
|
|
97
|
+
props: GreengrassNextVersionProps,
|
|
98
|
+
) {
|
|
99
|
+
super(scope, id);
|
|
100
|
+
|
|
101
|
+
if (!/^[a-zA-Z0-9-_.]+$/.test(props.componentName)) {
|
|
102
|
+
throw new Error('componentName must contain only letters, digits, hyphens, underscores and dots.');
|
|
103
|
+
}
|
|
104
|
+
const provider = GreengrassNextVersionProvider.ensure(this);
|
|
105
|
+
provider.allowComponent(props.componentName);
|
|
106
|
+
const resource = new CustomResource(this, 'Resource', {
|
|
107
|
+
serviceToken: provider.serviceToken,
|
|
108
|
+
resourceType: 'Custom::GreengrassNextVersion',
|
|
109
|
+
properties: {
|
|
110
|
+
ComponentName: props.componentName,
|
|
111
|
+
Strategy: props.strategy,
|
|
112
|
+
ContentSha256: props.contentSha256,
|
|
113
|
+
BucketName: props.bucketName,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
this.version = resource.getAttString('Version');
|
|
117
|
+
this.contentKey = resource.getAttString('ContentKey');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The artifact fields read or rewritten during component publication.
|
|
5
|
+
*/
|
|
6
|
+
export interface GreengrassRecipeArtifact {
|
|
7
|
+
readonly Uri: string;
|
|
8
|
+
readonly [key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GreengrassRecipeManifest {
|
|
12
|
+
readonly Artifacts?: readonly GreengrassRecipeArtifact[];
|
|
13
|
+
readonly [key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GreengrassRecipeSummary {
|
|
17
|
+
readonly componentName: string;
|
|
18
|
+
readonly componentVersion: string;
|
|
19
|
+
/**
|
|
20
|
+
* Parsed recipe; artifact URI substitution preserves all other fields.
|
|
21
|
+
*/
|
|
22
|
+
readonly raw: { Manifests?: GreengrassRecipeManifest[]; [key: string]: unknown };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the recipe's required ComponentName and ComponentVersion strings.
|
|
27
|
+
*/
|
|
28
|
+
export const readRecipeSummary = (
|
|
29
|
+
yamlSource: string,
|
|
30
|
+
recipePath: string,
|
|
31
|
+
): GreengrassRecipeSummary => {
|
|
32
|
+
const raw = yaml.load(yamlSource);
|
|
33
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
34
|
+
throw new Error(`"${recipePath}" is not a Greengrass recipe document`);
|
|
35
|
+
}
|
|
36
|
+
const { ComponentName, ComponentVersion } = raw as {
|
|
37
|
+
ComponentName?: unknown;
|
|
38
|
+
ComponentVersion?: unknown;
|
|
39
|
+
};
|
|
40
|
+
if (typeof ComponentName !== 'string' || ComponentName.length === 0) {
|
|
41
|
+
throw new Error(`"${recipePath}" is missing ComponentName`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof ComponentVersion !== 'string' || ComponentVersion.length === 0) {
|
|
44
|
+
throw new Error(`"${recipePath}" is missing ComponentVersion`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
componentName: ComponentName,
|
|
48
|
+
componentVersion: ComponentVersion,
|
|
49
|
+
raw: raw as GreengrassRecipeSummary['raw'],
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* GDK placeholder prefix identifying artifacts managed by this construct.
|
|
55
|
+
*/
|
|
56
|
+
const PLACEHOLDER_PREFIX = 's3://BUCKET_NAME/COMPONENT_NAME/COMPONENT_VERSION/';
|
|
57
|
+
|
|
58
|
+
export interface ArtifactUriSubstitution {
|
|
59
|
+
readonly bucketName: string;
|
|
60
|
+
/**
|
|
61
|
+
* Uploaded artifact key prefix, including the trailing slash.
|
|
62
|
+
*/
|
|
63
|
+
readonly keyPrefix: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Substitute managed artifact URIs in place, leaving other fields untouched.
|
|
68
|
+
* Return archive basenames; CDK resolves embedded bucket tokens at deployment.
|
|
69
|
+
*/
|
|
70
|
+
export const substituteArtifactUris = (
|
|
71
|
+
raw: GreengrassRecipeSummary['raw'],
|
|
72
|
+
substitution: ArtifactUriSubstitution,
|
|
73
|
+
): string[] => {
|
|
74
|
+
const { bucketName, keyPrefix } = substitution;
|
|
75
|
+
const replacementPrefix = `s3://${bucketName}/${keyPrefix}`;
|
|
76
|
+
const substitutedBasenames: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const manifest of raw.Manifests ?? []) {
|
|
79
|
+
for (const artifact of manifest.Artifacts ?? []) {
|
|
80
|
+
const mutableArtifact = artifact as { Uri: string };
|
|
81
|
+
if (mutableArtifact.Uri.startsWith(PLACEHOLDER_PREFIX)) {
|
|
82
|
+
const basename = mutableArtifact.Uri.slice(PLACEHOLDER_PREFIX.length);
|
|
83
|
+
substitutedBasenames.push(basename);
|
|
84
|
+
mutableArtifact.Uri = `${replacementPrefix}${basename}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return substitutedBasenames;
|
|
90
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a Greengrass component version from the registry at deployment
|
|
3
|
+
* time from the CDK stack.
|
|
4
|
+
* It computes a bucket-scoped content key, then inspects only the highest
|
|
5
|
+
* strict M.m.p version: reuse it only when its key matches and it is
|
|
6
|
+
* DEPLOYABLE; otherwise create the next version.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { createHash } = require('node:crypto');
|
|
10
|
+
|
|
11
|
+
const NEXT_VERSION_TAG_KEY = 'nx:greengrass:content-sha256';
|
|
12
|
+
const VERSIONING_TAG_KEY = 'nx:greengrass:versioning';
|
|
13
|
+
|
|
14
|
+
const parseStrictVersion = (version) => {
|
|
15
|
+
if (typeof version !== 'string') {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
19
|
+
if (!match) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
major: Number(match[1]),
|
|
24
|
+
minor: Number(match[2]),
|
|
25
|
+
patch: Number(match[3]),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const asParsedVersion = (version) =>
|
|
30
|
+
typeof version === 'string' ? parseStrictVersion(version) : version;
|
|
31
|
+
|
|
32
|
+
const compareVersions = (left, right) => {
|
|
33
|
+
const a = asParsedVersion(left);
|
|
34
|
+
const b = asParsedVersion(right);
|
|
35
|
+
if (!a || !b) {
|
|
36
|
+
throw new Error('Versions must be strict M.m.p values');
|
|
37
|
+
}
|
|
38
|
+
for (const field of ['major', 'minor', 'patch']) {
|
|
39
|
+
if (a[field] !== b[field]) {
|
|
40
|
+
return a[field] - b[field];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const bumpVersion = (version, strategy) => {
|
|
47
|
+
const parsed = parseStrictVersion(version);
|
|
48
|
+
if (!parsed) {
|
|
49
|
+
throw new Error(`Cannot bump non-strict version "${version}"`);
|
|
50
|
+
}
|
|
51
|
+
switch (strategy) {
|
|
52
|
+
case 'patch':
|
|
53
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
|
|
54
|
+
case 'minor':
|
|
55
|
+
return `${parsed.major}.${parsed.minor + 1}.0`;
|
|
56
|
+
case 'major':
|
|
57
|
+
return `${parsed.major + 1}.0.0`;
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported version strategy "${strategy}"`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const versionFromArn = (versionArn) => versionArn.split(':').at(-1);
|
|
64
|
+
|
|
65
|
+
const sha256hex = (value) =>
|
|
66
|
+
createHash('sha256').update(value).digest('hex');
|
|
67
|
+
|
|
68
|
+
const resolveNextVersion = async (api, input) => {
|
|
69
|
+
if (!input.componentName) {
|
|
70
|
+
return { version: '', action: 'none', highest: '', contentKey: '' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const contentKey = sha256hex(`${input.contentSha256}\n${input.bucketName}`);
|
|
74
|
+
const componentArn = `arn:${input.partition}:greengrass:${input.region}:${input.accountId}:components:${input.componentName}`;
|
|
75
|
+
const versionArns = await api.listVersions(componentArn);
|
|
76
|
+
const strictVersions = versionArns
|
|
77
|
+
.map((arn) => ({ arn, version: versionFromArn(arn) }))
|
|
78
|
+
.filter(({ version }) => parseStrictVersion(version));
|
|
79
|
+
const highestEntry = strictVersions.reduce(
|
|
80
|
+
(highest, candidate) =>
|
|
81
|
+
!highest || compareVersions(candidate.version, highest.version) > 0
|
|
82
|
+
? candidate
|
|
83
|
+
: highest,
|
|
84
|
+
undefined,
|
|
85
|
+
);
|
|
86
|
+
const highest = highestEntry?.version ?? '';
|
|
87
|
+
|
|
88
|
+
if (highestEntry) {
|
|
89
|
+
const described = await api.describeVersion(highestEntry.arn);
|
|
90
|
+
if (
|
|
91
|
+
described.state === 'DEPLOYABLE' &&
|
|
92
|
+
described.tags?.[NEXT_VERSION_TAG_KEY] === contentKey
|
|
93
|
+
) {
|
|
94
|
+
return { version: highest, action: 'reuse', highest, contentKey };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
version: highest ? bumpVersion(highest, input.strategy) : '1.0.0',
|
|
100
|
+
action: 'create',
|
|
101
|
+
highest,
|
|
102
|
+
contentKey,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const createGreengrassApi = ({ region }) => {
|
|
107
|
+
const {
|
|
108
|
+
DescribeComponentCommand,
|
|
109
|
+
GreengrassV2Client,
|
|
110
|
+
ListComponentVersionsCommand,
|
|
111
|
+
} = require('@aws-sdk/client-greengrassv2');
|
|
112
|
+
const client = new GreengrassV2Client({ region });
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
listVersions: async (componentArn) => {
|
|
116
|
+
const versions = [];
|
|
117
|
+
let nextToken;
|
|
118
|
+
do {
|
|
119
|
+
const response = await client.send(
|
|
120
|
+
new ListComponentVersionsCommand({
|
|
121
|
+
arn: componentArn,
|
|
122
|
+
nextToken,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
versions.push(
|
|
126
|
+
...(response.componentVersions ?? [])
|
|
127
|
+
.map((component) => component.arn)
|
|
128
|
+
.filter(Boolean),
|
|
129
|
+
);
|
|
130
|
+
nextToken = response.nextToken;
|
|
131
|
+
} while (nextToken);
|
|
132
|
+
return versions;
|
|
133
|
+
},
|
|
134
|
+
describeVersion: async (versionArn) => {
|
|
135
|
+
const response = await client.send(
|
|
136
|
+
new DescribeComponentCommand({ arn: versionArn }),
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
state: response.status?.componentState,
|
|
140
|
+
tags: response.tags ?? {},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const stackIdentity = (stackId) => {
|
|
147
|
+
const fields = String(stackId).split(':');
|
|
148
|
+
return {
|
|
149
|
+
partition: fields[1],
|
|
150
|
+
region: fields[3],
|
|
151
|
+
accountId: fields[4],
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
exports.NEXT_VERSION_TAG_KEY = NEXT_VERSION_TAG_KEY;
|
|
156
|
+
exports.VERSIONING_TAG_KEY = VERSIONING_TAG_KEY;
|
|
157
|
+
exports.parseStrictVersion = parseStrictVersion;
|
|
158
|
+
exports.compareVersions = compareVersions;
|
|
159
|
+
exports.bumpVersion = bumpVersion;
|
|
160
|
+
exports.resolveNextVersion = resolveNextVersion;
|
|
161
|
+
exports.createGreengrassApi = createGreengrassApi;
|
|
162
|
+
|
|
163
|
+
exports.handler = async (event) => {
|
|
164
|
+
if (event.RequestType === 'Delete') {
|
|
165
|
+
return { PhysicalResourceId: event.PhysicalResourceId };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const properties = event.ResourceProperties ?? {};
|
|
169
|
+
const identity = stackIdentity(event.StackId);
|
|
170
|
+
const input = {
|
|
171
|
+
componentName: properties.ComponentName,
|
|
172
|
+
strategy: properties.Strategy,
|
|
173
|
+
contentSha256: properties.ContentSha256,
|
|
174
|
+
bucketName: properties.BucketName,
|
|
175
|
+
region: identity.region,
|
|
176
|
+
accountId: identity.accountId,
|
|
177
|
+
partition: identity.partition,
|
|
178
|
+
};
|
|
179
|
+
const result = await resolveNextVersion(
|
|
180
|
+
input.componentName
|
|
181
|
+
? createGreengrassApi({ region: process.env.AWS_REGION })
|
|
182
|
+
: undefined,
|
|
183
|
+
input,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
PhysicalResourceId: `${properties.ComponentName}/next-version`,
|
|
188
|
+
Data: {
|
|
189
|
+
Version: result.version,
|
|
190
|
+
Action: result.action,
|
|
191
|
+
Highest: result.highest,
|
|
192
|
+
ContentKey: result.contentKey,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
variable "bucket_name" {
|
|
2
|
+
description = "Name of the shared artifact bucket this component version's artifact is uploaded to - typically the `bucket_name` output of a `core/greengrass/artifact-bucket` module, whose own `depends_on` orders this upload after the bucket is hardened."
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
# Publish "<%- componentDisplayName %>" from its <%- componentDirName %>-artifact build.
|
|
7
|
+
# Keep these paths aligned with the host project when editing this module.
|
|
8
|
+
module "component_version" {
|
|
9
|
+
source = "../../../core/greengrass/component-version"
|
|
10
|
+
|
|
11
|
+
recipes_dir = "${path.module}/../../../../../../../<%- recipesDirFromRoot %>"
|
|
12
|
+
artifacts_dir = "${path.module}/../../../../../../../<%- artifactsDirFromRoot %>"
|
|
13
|
+
bucket_name = var.bucket_name
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
output "component_name" {
|
|
17
|
+
value = module.component_version.component_name
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
output "component_version" {
|
|
21
|
+
value = module.component_version.component_version
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
output "arn" {
|
|
25
|
+
value = module.component_version.arn
|
|
26
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_providers {
|
|
3
|
+
aws = {
|
|
4
|
+
source = "hashicorp/aws"
|
|
5
|
+
version = "<%- awsProviderVersion %>"
|
|
6
|
+
}
|
|
7
|
+
random = {
|
|
8
|
+
source = "hashicorp/random"
|
|
9
|
+
version = "<%- randomProviderVersion %>"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
variable "bucket_name" {
|
|
15
|
+
description = "Physical name for a newly created bucket. Leave unset (null) to derive one from bucket_name_prefix instead. Ignored when existing_bucket_name is set."
|
|
16
|
+
type = string
|
|
17
|
+
default = null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
variable "bucket_name_prefix" {
|
|
21
|
+
description = "Prefix for the bucket name generated when bucket_name is left unset - typically the owning deployment's own name, so the created bucket carries workspace/deployment identity instead of an opaque AWS-generated name. Ignored when bucket_name or existing_bucket_name is set."
|
|
22
|
+
type = string
|
|
23
|
+
default = "greengrass-artifacts"
|
|
24
|
+
|
|
25
|
+
# Validate the prefix before it becomes part of an S3 bucket name.
|
|
26
|
+
validation {
|
|
27
|
+
condition = can(regex("^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", var.bucket_name_prefix))
|
|
28
|
+
error_message = "bucket_name_prefix must be lowercase letters, digits and hyphens only, starting and ending with a letter or digit - it becomes the leading segment of an S3 bucket name."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
variable "existing_bucket_name" {
|
|
33
|
+
description = "Name of an existing S3 bucket to import instead of creating one. When set, every other creation setting on this module is ignored - the imported bucket's encryption, versioning and public-access settings are whatever they already are."
|
|
34
|
+
type = string
|
|
35
|
+
default = null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
variable "token_exchange_role_arn" {
|
|
39
|
+
description = "ARN of a Greengrass core device token exchange role to grant s3:GetObject on this bucket, scoped to token_exchange_key_prefix. Leave unset to skip the grant - the guide documents the policy to attach by hand."
|
|
40
|
+
type = string
|
|
41
|
+
default = null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
variable "token_exchange_key_prefix" {
|
|
45
|
+
description = "Key prefix (glob) the token exchange role grant is scoped to. Defaults to the workspace component namespace; override for custom component names. Only used when token_exchange_role_arn is set."
|
|
46
|
+
type = string
|
|
47
|
+
default = "<%- componentKeyPrefix %>"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
variable "tags" {
|
|
51
|
+
description = "Tags to apply to the created bucket."
|
|
52
|
+
type = map(string)
|
|
53
|
+
default = {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
locals {
|
|
57
|
+
creating = var.existing_bucket_name == null
|
|
58
|
+
generating_name = local.creating && var.bucket_name == null
|
|
59
|
+
bucket_name = local.creating ? aws_s3_bucket.bucket[0].id : var.existing_bucket_name
|
|
60
|
+
bucket_arn = local.creating ? aws_s3_bucket.bucket[0].arn : data.aws_s3_bucket.existing[0].arn
|
|
61
|
+
|
|
62
|
+
# The account, region and stable random suffix fit within S3's 63-character
|
|
63
|
+
# limit after truncating the prefix to 20 characters.
|
|
64
|
+
generated_bucket_name = local.generating_name ? "${trimsuffix(substr(var.bucket_name_prefix, 0, 20), "-")}-${data.aws_caller_identity.current[0].account_id}-${data.aws_region.current[0].region}-${random_string.bucket_suffix[0].result}" : null
|
|
65
|
+
|
|
66
|
+
# Derive the imported role name from its ARN.
|
|
67
|
+
token_exchange_role_name_parts = var.token_exchange_role_arn != null ? split("/", var.token_exchange_role_arn) : []
|
|
68
|
+
token_exchange_role_name = length(local.token_exchange_role_name_parts) > 0 ? local.token_exchange_role_name_parts[length(local.token_exchange_role_name_parts) - 1] : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data "aws_s3_bucket" "existing" {
|
|
72
|
+
count = local.creating ? 0 : 1
|
|
73
|
+
bucket = var.existing_bucket_name
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Read identity only when generating a bucket name.
|
|
77
|
+
data "aws_caller_identity" "current" {
|
|
78
|
+
count = local.generating_name ? 1 : 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
data "aws_region" "current" {
|
|
82
|
+
count = local.generating_name ? 1 : 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resource "random_string" "bucket_suffix" {
|
|
86
|
+
count = local.generating_name ? 1 : 0
|
|
87
|
+
length = 8
|
|
88
|
+
special = false
|
|
89
|
+
upper = false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Own one private, versioned, encrypted, TLS-only repository per account and
|
|
93
|
+
# region; import it elsewhere with existing_bucket_name.
|
|
94
|
+
resource "aws_s3_bucket" "bucket" {
|
|
95
|
+
count = local.creating ? 1 : 0
|
|
96
|
+
#checkov:skip=CKV_AWS_18:Server access logs are not configured; artifacts are immutable published bytes and reads are auditable through CloudTrail data events
|
|
97
|
+
#checkov:skip=CKV2_AWS_61:No lifecycle configuration; a published component version's artifact must stay retrievable for as long as any deployment can reference it - see the guide's documented inventory/cleanup policy instead
|
|
98
|
+
#checkov:skip=CKV2_AWS_62:Event notifications not required for an artifact repository nothing subscribes to
|
|
99
|
+
#checkov:skip=CKV_AWS_144:Cross-region replication not required - component artifacts are published once per region they are deployed into
|
|
100
|
+
#checkov:skip=CKV_AWS_145:AES256 (S3-managed) encryption is sufficient for published component artifacts
|
|
101
|
+
bucket = coalesce(var.bucket_name, local.generated_bucket_name)
|
|
102
|
+
|
|
103
|
+
tags = merge(var.tags, { "greengrass-artifact-repository" = "true" })
|
|
104
|
+
|
|
105
|
+
# Protect the repository against replacement or teardown. Remove this literal
|
|
106
|
+
# lifecycle setting only for deliberate cleanup.
|
|
107
|
+
lifecycle {
|
|
108
|
+
prevent_destroy = true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
resource "aws_s3_bucket_versioning" "bucket" {
|
|
113
|
+
count = local.creating ? 1 : 0
|
|
114
|
+
bucket = aws_s3_bucket.bucket[0].id
|
|
115
|
+
versioning_configuration {
|
|
116
|
+
status = "Enabled"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "bucket" {
|
|
121
|
+
count = local.creating ? 1 : 0
|
|
122
|
+
bucket = aws_s3_bucket.bucket[0].id
|
|
123
|
+
|
|
124
|
+
rule {
|
|
125
|
+
apply_server_side_encryption_by_default {
|
|
126
|
+
sse_algorithm = "AES256"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
resource "aws_s3_bucket_public_access_block" "bucket" {
|
|
132
|
+
count = local.creating ? 1 : 0
|
|
133
|
+
bucket = aws_s3_bucket.bucket[0].id
|
|
134
|
+
|
|
135
|
+
block_public_acls = true
|
|
136
|
+
block_public_policy = true
|
|
137
|
+
ignore_public_acls = true
|
|
138
|
+
restrict_public_buckets = true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resource "aws_s3_bucket_policy" "bucket" {
|
|
142
|
+
count = local.creating ? 1 : 0
|
|
143
|
+
bucket = aws_s3_bucket.bucket[0].id
|
|
144
|
+
|
|
145
|
+
policy = jsonencode({
|
|
146
|
+
Version = "2012-10-17"
|
|
147
|
+
Statement = [
|
|
148
|
+
{
|
|
149
|
+
Sid = "DenyInsecureConnections"
|
|
150
|
+
Effect = "Deny"
|
|
151
|
+
Principal = "*"
|
|
152
|
+
Action = "s3:*"
|
|
153
|
+
Resource = [
|
|
154
|
+
aws_s3_bucket.bucket[0].arn,
|
|
155
|
+
"${aws_s3_bucket.bucket[0].arn}/*"
|
|
156
|
+
]
|
|
157
|
+
Condition = {
|
|
158
|
+
Bool = { "aws:SecureTransport" = "false" }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Grant the imported token exchange role read access to the selected key prefix.
|
|
166
|
+
resource "aws_iam_role_policy" "token_exchange_read" {
|
|
167
|
+
count = var.token_exchange_role_arn != null ? 1 : 0
|
|
168
|
+
|
|
169
|
+
name = "greengrass-artifact-read"
|
|
170
|
+
role = local.token_exchange_role_name
|
|
171
|
+
|
|
172
|
+
policy = jsonencode({
|
|
173
|
+
Version = "2012-10-17"
|
|
174
|
+
Statement = [
|
|
175
|
+
{
|
|
176
|
+
Sid = "GreengrassArtifactRead"
|
|
177
|
+
Effect = "Allow"
|
|
178
|
+
Action = "s3:GetObject"
|
|
179
|
+
Resource = "${local.bucket_arn}/${var.token_exchange_key_prefix}"
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Order consumers after the repository's hardening resources.
|
|
186
|
+
output "bucket_name" {
|
|
187
|
+
value = local.bucket_name
|
|
188
|
+
depends_on = [
|
|
189
|
+
aws_s3_bucket_versioning.bucket,
|
|
190
|
+
aws_s3_bucket_server_side_encryption_configuration.bucket,
|
|
191
|
+
aws_s3_bucket_public_access_block.bucket,
|
|
192
|
+
aws_s3_bucket_policy.bucket,
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
output "bucket_arn" {
|
|
197
|
+
value = local.bucket_arn
|
|
198
|
+
depends_on = [
|
|
199
|
+
aws_s3_bucket_versioning.bucket,
|
|
200
|
+
aws_s3_bucket_server_side_encryption_configuration.bucket,
|
|
201
|
+
aws_s3_bucket_public_access_block.bucket,
|
|
202
|
+
aws_s3_bucket_policy.bucket,
|
|
203
|
+
]
|
|
204
|
+
}
|