@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.
Files changed (150) hide show
  1. package/README.md +31 -29
  2. package/generators.json +16 -0
  3. package/migrations.json +6 -1
  4. package/package.json +1 -1
  5. package/src/agentcore-harness/generator.d.ts +1 -1
  6. package/src/greengrass/component/common.d.ts +60 -0
  7. package/src/greengrass/component/common.js +191 -0
  8. package/src/greengrass/component/common.js.map +1 -0
  9. package/src/greengrass/component/dependencies.d.ts +101 -0
  10. package/src/greengrass/component/dependencies.js +37 -0
  11. package/src/greengrass/component/dependencies.js.map +1 -0
  12. package/src/greengrass/component/generator.d.ts +174 -0
  13. package/src/greengrass/component/generator.js +32 -0
  14. package/src/greengrass/component/generator.js.map +1 -0
  15. package/src/greengrass/component/metadata.d.ts +2 -0
  16. package/src/greengrass/component/metadata.js +14 -0
  17. package/src/greengrass/component/metadata.js.map +1 -0
  18. package/src/greengrass/component/python/__snapshots__/generator.spec.ts.snap +450 -0
  19. package/src/greengrass/component/python/files/component/main.py.template +60 -0
  20. package/src/greengrass/component/python/files/component/recipe.yaml.template +30 -0
  21. package/src/greengrass/component/python/files/gdk/gdk-config.json.template +13 -0
  22. package/src/greengrass/component/python/files/tests/test___componentSnakeCase__.py.template +37 -0
  23. package/src/greengrass/component/python/generator.d.ts +15 -0
  24. package/src/greengrass/component/python/generator.js +192 -0
  25. package/src/greengrass/component/python/generator.js.map +1 -0
  26. package/src/greengrass/component/schema.d.js +6 -0
  27. package/src/greengrass/component/schema.d.js.map +1 -0
  28. package/src/greengrass/component/schema.d.ts +18 -0
  29. package/src/greengrass/component/schema.json +82 -0
  30. package/src/greengrass/component/typescript/__snapshots__/generator.spec.ts.snap +275 -0
  31. package/src/greengrass/component/typescript/files/component/recipe.yaml.template +41 -0
  32. package/src/greengrass/component/typescript/files/entrypoint/main.ts.template +58 -0
  33. package/src/greengrass/component/typescript/files/gdk/gdk-config.json.template +13 -0
  34. package/src/greengrass/component/typescript/files/scripts/vendor-crt-addon.ts.template +115 -0
  35. package/src/greengrass/component/typescript/generator.d.ts +16 -0
  36. package/src/greengrass/component/typescript/generator.js +194 -0
  37. package/src/greengrass/component/typescript/generator.js.map +1 -0
  38. package/src/infra/app/generator.d.ts +1 -1
  39. package/src/init/generator.d.ts +1 -1
  40. package/src/internal/test-matrix/generator.js +48 -1
  41. package/src/internal/test-matrix/generator.js.map +1 -1
  42. package/src/license/dependency-check/pre-approved.js +2 -1
  43. package/src/license/dependency-check/pre-approved.js.map +1 -1
  44. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/metadata.json +3 -0
  45. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
  46. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
  47. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
  48. package/src/open-api/py-client/generator.d.ts +1 -1
  49. package/src/preset/__snapshots__/generator.spec.ts.snap +4 -0
  50. package/src/preset/generator.d.ts +1 -1
  51. package/src/py/agent/a2a-connection/generator.d.ts +1 -1
  52. package/src/py/agent/gateway-connection/generator.d.ts +1 -1
  53. package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
  54. package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
  55. package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
  56. package/src/py/agent/guardrails/generator.d.ts +10 -0
  57. package/src/py/agent/guardrails/generator.js +175 -0
  58. package/src/py/agent/guardrails/generator.js.map +1 -0
  59. package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
  60. package/src/py/agent/guardrails/schema.d.js +6 -0
  61. package/src/py/agent/guardrails/schema.d.js.map +1 -0
  62. package/src/py/agent/guardrails/schema.d.ts +9 -0
  63. package/src/py/agent/guardrails/schema.json +25 -0
  64. package/src/py/agent/mcp-connection/generator.d.ts +1 -1
  65. package/src/py/agent/react-connection/generator.d.ts +1 -1
  66. package/src/py/fast-api/react/generator.d.ts +1 -1
  67. package/src/py/project/generator.js +2 -0
  68. package/src/py/project/generator.js.map +1 -1
  69. package/src/sdk/greengrass.d.ts +6 -0
  70. package/src/sdk/greengrass.js +6 -0
  71. package/src/sdk/greengrass.js.map +1 -0
  72. package/src/sdk/py.d.ts +2 -0
  73. package/src/sdk/py.js +1 -0
  74. package/src/sdk/py.js.map +1 -1
  75. package/src/smithy/project/generator.d.ts +1 -1
  76. package/src/smithy/react-connection/generator.d.ts +1 -1
  77. package/src/terraform/project/generator.d.ts +1 -1
  78. package/src/trpc/react/generator.d.ts +1 -1
  79. package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
  80. package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
  81. package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +54 -3
  82. package/src/ts/agent/guardrails/generator.js +2 -1
  83. package/src/ts/agent/guardrails/generator.js.map +1 -1
  84. package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
  85. package/src/ts/agent/react-connection/generator.d.ts +1 -1
  86. package/src/ts/astro-docs/generator.d.ts +1 -1
  87. package/src/ts/dcr-proxy/generator.d.ts +1 -1
  88. package/src/ts/dynamodb/generator.d.ts +1 -1
  89. package/src/ts/lambda-function/generator.d.ts +1 -1
  90. package/src/ts/lib/generator.d.ts +1 -1
  91. package/src/ts/nx-generator/generator.d.ts +1 -1
  92. package/src/ts/nx-migration/generator.d.ts +1 -1
  93. package/src/ts/nx-plugin/generator.d.ts +1 -1
  94. package/src/ts/rdb/generator.d.ts +1 -1
  95. package/src/ts/react-website/agui/generator.d.ts +1 -1
  96. package/src/utils/__snapshots__/shared-greengrass-scripts.spec.ts.snap +1279 -0
  97. package/src/utils/files/common/scripts/src/greengrass/build-artifact.ts.template +236 -0
  98. package/src/utils/files/common/scripts/src/greengrass/component-logs.ts.template +48 -0
  99. package/src/utils/files/common/scripts/src/greengrass/deploy-local.ts.template +497 -0
  100. package/src/utils/files/common/scripts/src/greengrass/recipe-utils.ts.template +356 -0
  101. package/src/utils/files/common/scripts/src/greengrass/zip-writer.ts.template +121 -0
  102. package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +3 -3
  103. package/src/utils/files/terraform/src/metrics/metrics.tf.template +1 -1
  104. package/src/utils/greengrass/constants.d.ts +58 -0
  105. package/src/utils/greengrass/constants.js +63 -0
  106. package/src/utils/greengrass/constants.js.map +1 -0
  107. package/src/utils/greengrass/naming.d.ts +46 -0
  108. package/src/utils/greengrass/naming.js +67 -0
  109. package/src/utils/greengrass/naming.js.map +1 -0
  110. package/src/utils/greengrass/recipe.d.ts +143 -0
  111. package/src/utils/greengrass/recipe.js +119 -0
  112. package/src/utils/greengrass/recipe.js.map +1 -0
  113. package/src/utils/greengrass-constructs/files/cdk/app/greengrass-component/__componentDirName__.ts.template +42 -0
  114. package/src/utils/greengrass-constructs/files/cdk/core/greengrass/artifact-bucket.ts.template +82 -0
  115. package/src/utils/greengrass-constructs/files/cdk/core/greengrass/component-version.ts.template +370 -0
  116. package/src/utils/greengrass-constructs/files/cdk/core/greengrass/deployment.ts.template +276 -0
  117. package/src/utils/greengrass-constructs/files/cdk/core/greengrass/index.ts.template +5 -0
  118. package/src/utils/greengrass-constructs/files/cdk/core/greengrass/next-version.ts.template +119 -0
  119. package/src/utils/greengrass-constructs/files/cdk/core/greengrass/recipe.ts.template +90 -0
  120. package/src/utils/greengrass-constructs/files/next-version/__nextVersionFileName__.template +195 -0
  121. package/src/utils/greengrass-constructs/files/terraform/app/greengrass-component/__componentDirName__/__componentDirName__.tf.template +26 -0
  122. package/src/utils/greengrass-constructs/files/terraform/core/greengrass/artifact-bucket/main.tf.template +204 -0
  123. package/src/utils/greengrass-constructs/files/terraform/core/greengrass/component-version/main.tf.template +156 -0
  124. package/src/utils/greengrass-constructs/files/terraform/core/greengrass/deployment/main.tf.template +186 -0
  125. package/src/utils/greengrass-constructs/greengrass-constructs.d.ts +57 -0
  126. package/src/utils/greengrass-constructs/greengrass-constructs.js +154 -0
  127. package/src/utils/greengrass-constructs/greengrass-constructs.js.map +1 -0
  128. package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
  129. package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
  130. package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
  131. package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
  132. package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
  133. package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
  134. package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
  135. package/src/utils/nx.d.ts +15 -0
  136. package/src/utils/nx.js +32 -0
  137. package/src/utils/nx.js.map +1 -1
  138. package/src/utils/shared-greengrass-scripts.d.ts +21 -0
  139. package/src/utils/shared-greengrass-scripts.js +43 -0
  140. package/src/utils/shared-greengrass-scripts.js.map +1 -0
  141. package/src/utils/test/fake-aws/fake_aws.py +205 -0
  142. package/src/utils/test/python-dependencies.d.ts +6 -0
  143. package/src/utils/test/python-dependencies.js +19 -0
  144. package/src/utils/test/python-dependencies.js.map +1 -1
  145. package/src/utils/test/warm-python-cache.js +5 -1
  146. package/src/utils/test/warm-python-cache.js.map +1 -1
  147. package/src/utils/versions.d.ts +7 -0
  148. package/src/utils/versions.js +7 -0
  149. package/src/utils/versions.js.map +1 -1
  150. /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
+ }