@intentius/chant-lexicon-aws 0.124.46 → 0.124.49
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/dist/components/capability-plugin.d.ts +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +6 -4
- package/dist/components/publish.d.ts +28 -97
- package/dist/components/publish.d.ts.map +1 -1
- package/dist/components/publish.js +42 -109
- package/dist/import/parser.d.ts +2 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/parser.js +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +17 -0
- package/dist/workspace-member.d.ts +34 -0
- package/dist/workspace-member.d.ts.map +1 -0
- package/dist/workspace-member.js +179 -0
- package/package.json +6 -3
- package/schemas/cdk.schema.json +14 -0
- package/src/components/capability-plugin.ts +6 -4
- package/src/components/publish.ts +61 -193
- package/src/import/parser.ts +1 -1
- package/src/plugin.ts +16 -0
- package/src/testdata/cdk-member/cdk.json +1 -0
- package/src/testdata/cdk-member/cdk.out/AppStack.template.json +20 -0
- package/src/testdata/cdk-member/cdk.out/manifest.json +1 -0
- package/src/testdata/cdk-unsynthesized/cdk.json +1 -0
- package/src/testdata/cfn-member/README.txt +1 -0
- package/src/testdata/cfn-member/stack.yaml +36 -0
- package/src/workspace-kinds.test.ts +47 -0
- package/src/workspace-member.test.ts +94 -0
- package/src/workspace-member.ts +197 -0
- package/workspace-kinds.json +66 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cloudformation workspace member into the graph (#3179).
|
|
3
|
+
*
|
|
4
|
+
* `chant workspace graph` writes a reader project for a member of kind
|
|
5
|
+
* `cloudformation`, with `aws.member` in its config (see `workspace-kinds.json`).
|
|
6
|
+
* The plugin's `buildRoots` hook calls {@link readCloudFormationMember}: each
|
|
7
|
+
* CloudFormation template directly in the member's directory is parsed with the
|
|
8
|
+
* import parser's YAML schema (short-form tags included), and every resource
|
|
9
|
+
* becomes a node keyed `<label>/<LogicalId>`.
|
|
10
|
+
*
|
|
11
|
+
* Edges come from what the template says: `Ref`, `Fn::GetAtt` and the `${...}`
|
|
12
|
+
* references in `Fn::Sub`, wherever they sit in a resource's properties, plus
|
|
13
|
+
* `DependsOn`. Parameters, pseudo parameters and names that are not a resource
|
|
14
|
+
* of the same template (`AWS::Region`, an `Fn::ImportValue`) draw no edge.
|
|
15
|
+
* Nothing runs and no AWS call is made.
|
|
16
|
+
*
|
|
17
|
+
* A `cdk` member is read the same way from the cloud assembly its app already
|
|
18
|
+
* synthesized (`cdk.out`, or the `output` its cdk.json names): the assembly's
|
|
19
|
+
* `*.template.json` files are CloudFormation templates, and core's CDK
|
|
20
|
+
* assembly reader says whether the directory is one. chant does not run the
|
|
21
|
+
* app. With no assembly there is nothing to read, and the report says
|
|
22
|
+
* `synthesized: false` with the reason.
|
|
23
|
+
*/
|
|
24
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
25
|
+
import { basename, extname, join } from "node:path";
|
|
26
|
+
import yaml from "js-yaml";
|
|
27
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
28
|
+
import { isCloudAssembly } from "@intentius/chant/cdk/assembly";
|
|
29
|
+
import { CF_SCHEMA } from "./import/parser.js";
|
|
30
|
+
function parseTemplate(text) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
try {
|
|
37
|
+
parsed = yaml.load(text, { schema: CF_SCHEMA });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
44
|
+
return undefined;
|
|
45
|
+
const t = parsed;
|
|
46
|
+
// The probe's two claims: a format version, or a Resources section.
|
|
47
|
+
const resources = t.Resources;
|
|
48
|
+
if (t.AWSTemplateFormatVersion === undefined && (!resources || typeof resources !== "object"))
|
|
49
|
+
return undefined;
|
|
50
|
+
return t;
|
|
51
|
+
}
|
|
52
|
+
/** Logical ids named by `${X}` and `${X.Attr}` in an `Fn::Sub` string, with the attribute when one is named. */
|
|
53
|
+
function subReferences(text) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const m of text.matchAll(/\$\{([^}!][^}]*)\}/g)) {
|
|
56
|
+
const [id, ...rest] = m[1].trim().split(".");
|
|
57
|
+
if (id)
|
|
58
|
+
out.push({ id, ...(rest.length ? { attr: rest.join(".") } : {}) });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** Every name a value reaches through Ref, Fn::GetAtt or Fn::Sub. */
|
|
63
|
+
function collectNames(value, out) {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
for (const v of value)
|
|
66
|
+
collectNames(v, out);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!value || typeof value !== "object")
|
|
70
|
+
return;
|
|
71
|
+
for (const [k, v] of Object.entries(value)) {
|
|
72
|
+
if (k === "Ref" && typeof v === "string") {
|
|
73
|
+
out.push({ id: v });
|
|
74
|
+
}
|
|
75
|
+
else if (k === "Fn::GetAtt") {
|
|
76
|
+
const [id, attr] = Array.isArray(v) ? v : typeof v === "string" ? [v.slice(0, v.indexOf(".")), v.slice(v.indexOf(".") + 1)] : [];
|
|
77
|
+
if (typeof id === "string" && id)
|
|
78
|
+
out.push({ id, ...(typeof attr === "string" && attr ? { attr } : {}) });
|
|
79
|
+
}
|
|
80
|
+
else if (k === "Fn::Sub") {
|
|
81
|
+
const text = Array.isArray(v) ? v[0] : v;
|
|
82
|
+
if (typeof text === "string")
|
|
83
|
+
out.push(...subReferences(text));
|
|
84
|
+
if (Array.isArray(v))
|
|
85
|
+
collectNames(v[1], out);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
collectNames(v, out);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** The directory a CDK app synthesizes into: cdk.json's `output`, else `cdk.out`. */
|
|
93
|
+
function cdkOutput(dir) {
|
|
94
|
+
try {
|
|
95
|
+
const output = JSON.parse(readFileSync(join(dir, "cdk.json"), "utf-8")).output;
|
|
96
|
+
if (typeof output === "string" && output)
|
|
97
|
+
return output;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* no readable cdk.json: the default */
|
|
101
|
+
}
|
|
102
|
+
return "cdk.out";
|
|
103
|
+
}
|
|
104
|
+
export function readCloudFormationMember(opts) {
|
|
105
|
+
const { member } = opts;
|
|
106
|
+
const label = member.label ?? basename(member.dir);
|
|
107
|
+
const warnings = [];
|
|
108
|
+
const templates = [];
|
|
109
|
+
let source = member.dir;
|
|
110
|
+
if (member.kind === "cdk") {
|
|
111
|
+
source = join(member.dir, cdkOutput(member.dir));
|
|
112
|
+
if (!isCloudAssembly(source)) {
|
|
113
|
+
const reason = `no synthesized cloud assembly at ${source}; run \`cdk synth\` in the app, and chant will read the templates it writes`;
|
|
114
|
+
warnings.push(`cdk member "${label}" was not read: ${reason}`);
|
|
115
|
+
return { entities: new Map(), warnings, report: { templates: [], objects: 0, synthesized: false, reason } };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const pattern = member.kind === "cdk" ? /\.template\.json$/ : /\.(ya?ml|json|template)$/;
|
|
119
|
+
for (const file of readdirSync(source).filter((f) => pattern.test(f)).sort()) {
|
|
120
|
+
const template = parseTemplate(readFileSync(join(source, file), "utf-8"));
|
|
121
|
+
if (template)
|
|
122
|
+
templates.push({ file, template });
|
|
123
|
+
}
|
|
124
|
+
const entities = new Map();
|
|
125
|
+
for (const { file, template } of templates) {
|
|
126
|
+
// One template per member is the usual case and keeps ids short; two share a directory, so the file name qualifies them.
|
|
127
|
+
const prefix = templates.length === 1 ? label : `${label}/${basename(file, extname(file)).replace(/\.template$/, "")}`;
|
|
128
|
+
const resources = Object.entries(template.Resources ?? {}).filter(([, r]) => typeof r?.Type === "string");
|
|
129
|
+
const keyOf = new Map(resources.map(([id]) => [id, `${prefix}/${id}`]));
|
|
130
|
+
for (const [id, resource] of resources) {
|
|
131
|
+
const references = new Map();
|
|
132
|
+
const add = (name, via) => {
|
|
133
|
+
const to = keyOf.get(name.id);
|
|
134
|
+
if (!to || to === keyOf.get(id))
|
|
135
|
+
return;
|
|
136
|
+
references.set(`${to}|${via}|${name.attr ?? ""}`, { to, viaAttr: via, ...(name.attr ? { toAttr: name.attr } : {}) });
|
|
137
|
+
};
|
|
138
|
+
for (const [prop, value] of Object.entries(resource.Properties ?? {})) {
|
|
139
|
+
const names = [];
|
|
140
|
+
collectNames(value, names);
|
|
141
|
+
for (const n of names)
|
|
142
|
+
add(n, prop);
|
|
143
|
+
}
|
|
144
|
+
const dependsOn = resource.DependsOn;
|
|
145
|
+
for (const d of Array.isArray(dependsOn) ? dependsOn : dependsOn === undefined ? [] : [dependsOn]) {
|
|
146
|
+
if (typeof d === "string")
|
|
147
|
+
add({ id: d }, "DependsOn");
|
|
148
|
+
}
|
|
149
|
+
const entity = {
|
|
150
|
+
lexicon: "aws",
|
|
151
|
+
entityType: resource.Type,
|
|
152
|
+
kind: "resource",
|
|
153
|
+
props: { ...(resource.Properties ?? {}) },
|
|
154
|
+
references: [...references.values()],
|
|
155
|
+
[DECLARABLE_MARKER]: true,
|
|
156
|
+
};
|
|
157
|
+
entities.set(keyOf.get(id), entity);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (templates.length === 0)
|
|
161
|
+
warnings.push(`${member.kind ?? "cloudformation"} member "${label}" has no CloudFormation template directly in ${source}`);
|
|
162
|
+
return {
|
|
163
|
+
entities,
|
|
164
|
+
warnings,
|
|
165
|
+
report: { templates: templates.map((t) => t.file), objects: entities.size, ...(member.kind === "cdk" ? { synthesized: true } : {}) },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
let lastReport;
|
|
169
|
+
/**
|
|
170
|
+
* The report of the read in this process. `graphMeta` receives only the
|
|
171
|
+
* entities, so the hook that read the member leaves its report here for the
|
|
172
|
+
* hook that publishes it; `chant graph` runs the two in order in one process.
|
|
173
|
+
*/
|
|
174
|
+
export function setLastMemberReport(report) {
|
|
175
|
+
lastReport = report;
|
|
176
|
+
}
|
|
177
|
+
export function lastMemberReport() {
|
|
178
|
+
return lastReport;
|
|
179
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.49",
|
|
4
4
|
"description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
"type": "module",
|
|
24
24
|
"files": [
|
|
25
25
|
"src/",
|
|
26
|
-
"dist/"
|
|
26
|
+
"dist/",
|
|
27
|
+
"schemas/",
|
|
28
|
+
"workspace-kinds.json"
|
|
27
29
|
],
|
|
28
30
|
"publishConfig": {
|
|
29
31
|
"access": "public"
|
|
30
32
|
},
|
|
31
33
|
"exports": {
|
|
34
|
+
"./workspace-kinds": "./workspace-kinds.json",
|
|
32
35
|
".": {
|
|
33
36
|
"development": "./src/index.ts",
|
|
34
37
|
"types": "./dist/index.d.ts",
|
|
@@ -80,6 +83,6 @@
|
|
|
80
83
|
"typescript": "^5.9.3"
|
|
81
84
|
},
|
|
82
85
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.124.
|
|
86
|
+
"@intentius/chant": "^0.124.49"
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { Capability } from "@intentius/chant/components/capability";
|
|
12
12
|
import { ownPackageVersion, type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { ecrRegistryBackend, s3StorageBackend, loadImageOnHostCapability } from "./publish";
|
|
15
15
|
import { extractConfigBomCapability } from "./config-bom";
|
|
16
16
|
import {
|
|
17
17
|
cfnDeployCapability,
|
|
@@ -30,7 +30,9 @@ import { snapshotBeforeCapability, rollbackPreviousCapability } from "./safety";
|
|
|
30
30
|
export const AWS_VERB_FAMILIES = {
|
|
31
31
|
// `extract-config-bom` parses a synthesized CloudFormation template (#684).
|
|
32
32
|
sbom: ["extract-config-bom"],
|
|
33
|
-
|
|
33
|
+
// `publish-image` is core's verb (#3142, #3143); aws contributes its ECR login as a registry backend.
|
|
34
|
+
// `publish-artifact` is core's verb (#4408); aws contributes S3 as a storage backend.
|
|
35
|
+
publish: ["load-image-on-host"],
|
|
34
36
|
apply: ["cfn-deploy", "ecs-update-service", "lambda-deploy", "s3-sync", "cdn-invalidate", "run-migration"],
|
|
35
37
|
jobSubmission: ["emr-start-job-run", "emr-submit-step"],
|
|
36
38
|
hostDelivery: ["code-deploy", "copy-to-host", "remote-exec"],
|
|
@@ -42,9 +44,7 @@ export const AWS_VERB_FAMILIES = {
|
|
|
42
44
|
function awsCapabilities(): Array<Capability<never, unknown>> {
|
|
43
45
|
return [
|
|
44
46
|
extractConfigBomCapability,
|
|
45
|
-
publishImageCapability,
|
|
46
47
|
loadImageOnHostCapability,
|
|
47
|
-
publishArtifactCapability,
|
|
48
48
|
cfnDeployCapability,
|
|
49
49
|
ecsUpdateServiceCapability,
|
|
50
50
|
lambdaDeployCapability,
|
|
@@ -75,5 +75,7 @@ export const awsCapabilityPlugin: CapabilityPlugin = {
|
|
|
75
75
|
return ownPackageVersion(import.meta.url);
|
|
76
76
|
},
|
|
77
77
|
capabilities: awsCapabilities,
|
|
78
|
+
registryBackends: () => [ecrRegistryBackend],
|
|
79
|
+
storageBackends: () => [s3StorageBackend],
|
|
78
80
|
families: () => AWS_VERB_FAMILIES,
|
|
79
81
|
};
|
|
@@ -46,189 +46,64 @@
|
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
import type { Capability } from "@intentius/chant/components/capability";
|
|
49
|
-
import { readFileSync } from "node:fs";
|
|
50
|
-
import { createHash } from "node:crypto";
|
|
51
49
|
import { defaultCloudExecutor, type CloudExecutor } from "./cloud-executor";
|
|
52
50
|
import { archiveRelativePath } from "@intentius/chant/components/verbs/build-archive";
|
|
53
|
-
import { defaultProcessRunner,
|
|
51
|
+
import { defaultProcessRunner, type ProcessRunner } from "@intentius/chant/components/verbs/process-runner";
|
|
52
|
+
import {
|
|
53
|
+
createPublishImageCapability as createCorePublishImage,
|
|
54
|
+
type PublishImageBackend,
|
|
55
|
+
type RegistryLoginBackend,
|
|
56
|
+
} from "@intentius/chant/components/verbs/publish-image";
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
createPublishArtifactCapability as createCorePublishArtifact,
|
|
60
|
+
type ArtifactStorageBackend,
|
|
61
|
+
type PublishArtifactInput,
|
|
62
|
+
type PublishArtifactOutput,
|
|
63
|
+
} from "@intentius/chant/components/verbs/publish-artifact";
|
|
54
64
|
|
|
55
65
|
// ── shared backend interface ─────────────────────────────────────────────────
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* `selectPublishBackend` below) reads whichever field it needs — the
|
|
64
|
-
* unselected backend's field is simply unused, never a pipeline fork.
|
|
65
|
-
*/
|
|
66
|
-
export interface PublishImageInput {
|
|
67
|
-
/** Path of the image tarball inside the build archive (as produced by `docker-build`; an `archive:`-prefixed reference is accepted and stripped). */
|
|
68
|
-
from: string;
|
|
69
|
-
/** Destination registry — required by `publish-image`, ignored by `load-image-on-host` (e.g. `$env.registry` resolved by the orchestrator). */
|
|
70
|
-
to?: string;
|
|
71
|
-
/** Target host — required by `load-image-on-host`, ignored by `publish-image` (SSM instance id, hostname, or host group). */
|
|
72
|
-
host?: string;
|
|
73
|
-
/** Destination path for the tarball on the host, used only by `load-image-on-host`. Default: `/tmp/chant-archive/<basename of from>`. */
|
|
74
|
-
hostPath?: string;
|
|
75
|
-
/** Additional tags to apply alongside the digest, used only by `publish-image`. */
|
|
76
|
-
tags?: string[];
|
|
77
|
-
/**
|
|
78
|
-
* #610: the artifact's software SBOM (typically wired from a prior
|
|
79
|
-
* `generate-sbom` step's `sbom` output — see ./sbom.ts's
|
|
80
|
-
* `GenerateSbomOutput`) to attach as an OCI referrer on the pushed image
|
|
81
|
-
* digest, via `oras attach`. Used only by `publish-image`; ignored by
|
|
82
|
-
* `load-image-on-host` (registry-less — nothing to attach to). Omit to
|
|
83
|
-
* skip the attach step entirely (the archive-carried copy remains the
|
|
84
|
-
* SBOM's home either way).
|
|
85
|
-
*/
|
|
86
|
-
sbom?: { bytes: string; mediaType: string };
|
|
87
|
-
/**
|
|
88
|
-
* #610: the component-level aggregate BOM (wired from
|
|
89
|
-
* ./component-bom.ts's `aggregateComponentBom` output), attached as a
|
|
90
|
-
* second OCI referrer alongside `sbom` when supplied. Optional and
|
|
91
|
-
* independent of `sbom` — a caller may attach either, both, or neither.
|
|
92
|
-
*/
|
|
93
|
-
componentBom?: { bytes: string; mediaType: string };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Result of `publish-image`'s best-effort OCI-referrer attach step (#610) — always present when `input.sbom`/`input.componentBom` was supplied, `undefined` when neither was (nothing to attach, attach step skipped entirely). */
|
|
97
|
-
export interface ReferrerAttachResult {
|
|
98
|
-
/** True once every requested attach (`sbom`, `componentBom`) succeeded. `false` if `oras` was unavailable or any `oras attach` invocation failed. */
|
|
99
|
-
attached: boolean;
|
|
100
|
-
/** Human-readable reason `attached` is `false` (e.g. "oras is not installed"), omitted when `attached` is `true`. */
|
|
101
|
-
reason?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface PublishImageOutput {
|
|
105
|
-
/** Content-addressed digest of the promoted image (`sha256:...`) — what the apply step references. */
|
|
106
|
-
digest: string;
|
|
107
|
-
/** Image reference the apply step can pull/run: `registry/repo@sha256:...` for `publish-image`, a host-local reference for `load-image-on-host`. */
|
|
108
|
-
uri: string;
|
|
109
|
-
/** #610: outcome of the best-effort SBOM/component-BOM referrer attach, when `input.sbom`/`input.componentBom` was supplied. `undefined` for `load-image-on-host` (no attach concept — registry-less) and for `publish-image` calls that supplied neither. */
|
|
110
|
-
referrerAttach?: ReferrerAttachResult;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Common shape both image-publish backends satisfy: promote the image bytes
|
|
115
|
-
* held in the build archive to wherever the deploy target can consume them,
|
|
116
|
-
* and return the identity the apply step references. The backend is
|
|
117
|
-
* selected per environment (`selectPublishBackend`), never per component.
|
|
118
|
-
*/
|
|
119
|
-
export type PublishImageBackend = Capability<PublishImageInput, PublishImageOutput>;
|
|
67
|
+
export type {
|
|
68
|
+
PublishImageInput,
|
|
69
|
+
PublishImageOutput,
|
|
70
|
+
PublishImageBackend,
|
|
71
|
+
ReferrerAttachResult,
|
|
72
|
+
} from "@intentius/chant/components/verbs/publish-image";
|
|
120
73
|
|
|
121
|
-
// ── publish-image (
|
|
74
|
+
// ── publish-image: the ECR backend (the verb itself is core's) ──────────────
|
|
122
75
|
|
|
123
76
|
/**
|
|
124
|
-
*
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
127
|
-
* `runner`. Never throws — a failure here is reported back to the caller as
|
|
128
|
-
* `{ ok: false, reason }` so a missing/erroring `oras` never fails the
|
|
129
|
-
* publish itself (see `attachReferrers`'s doc comment for why).
|
|
77
|
+
* The aws lexicon's registry login for core's `publish-image` (#3142, #3143):
|
|
78
|
+
* `aws ecr get-login-password | docker login`. The aws plugin contributes it;
|
|
79
|
+
* core owns load, tag, push and the digest read-back.
|
|
130
80
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const scratchFile = `/tmp/chant-referrer-attach/${label}-${Date.now()}.json`;
|
|
139
|
-
await runner.run(`mkdir -p ${q("/tmp/chant-referrer-attach")}`);
|
|
140
|
-
await runner.run(`printf '%s' ${q(doc.bytes)} > ${q(scratchFile)}`);
|
|
141
|
-
await runner.run(`oras attach --artifact-type ${q(doc.mediaType)} ${q(imageRef)} ${q(scratchFile)}:${q(doc.mediaType)}`);
|
|
142
|
-
return { ok: true };
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
145
|
-
}
|
|
81
|
+
export function createEcrRegistryBackend(executor?: CloudExecutor): RegistryLoginBackend {
|
|
82
|
+
return {
|
|
83
|
+
name: "ecr",
|
|
84
|
+
matches: (registry) => /\.dkr\.ecr(-fips)?\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$/.test(registry),
|
|
85
|
+
// The real executor is built on first login, so loading the plugin shells out to nothing.
|
|
86
|
+
login: (registry) => (executor ?? defaultCloudExecutor()).ecr.login(registry),
|
|
87
|
+
};
|
|
146
88
|
}
|
|
147
89
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
* `input.sbom` and/or `input.componentBom`, when supplied, to `imageRef` via
|
|
151
|
-
* `oras attach`. Guarded on `oras` availability — a missing binary is
|
|
152
|
-
* reported back as `{ attached: false, reason }` rather than thrown, since
|
|
153
|
-
* the image itself is already successfully promoted by the time this step
|
|
154
|
-
* runs (attach is additive registry-side convenience on top of the
|
|
155
|
-
* archive-carried copy, per this module's doc comment, never a condition the
|
|
156
|
-
* core promote-by-digest job depends on). Returns `undefined` when neither
|
|
157
|
-
* `input.sbom` nor `input.componentBom` was supplied — nothing to attach,
|
|
158
|
-
* skipped entirely, no `oras` call at all.
|
|
159
|
-
*/
|
|
160
|
-
async function attachReferrers(
|
|
161
|
-
runner: ProcessRunner,
|
|
162
|
-
imageRef: string,
|
|
163
|
-
input: Pick<PublishImageInput, "sbom" | "componentBom">,
|
|
164
|
-
): Promise<ReferrerAttachResult | undefined> {
|
|
165
|
-
if (!input.sbom && !input.componentBom) return undefined;
|
|
166
|
-
|
|
167
|
-
if (!(await runner.available("oras"))) {
|
|
168
|
-
return { attached: false, reason: `"oras" is not installed or not on PATH — required to attach OCI referrers` };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const results: Array<{ ok: boolean; reason?: string }> = [];
|
|
172
|
-
if (input.sbom) results.push(await attachOneReferrer(runner, imageRef, input.sbom, "sbom"));
|
|
173
|
-
if (input.componentBom) results.push(await attachOneReferrer(runner, imageRef, input.componentBom, "component-bom"));
|
|
174
|
-
|
|
175
|
-
const failed = results.find((r) => !r.ok);
|
|
176
|
-
return failed ? { attached: false, reason: failed.reason } : { attached: true };
|
|
177
|
-
}
|
|
90
|
+
/** The ECR backend the aws plugin contributes, over the real `CloudExecutor`. */
|
|
91
|
+
export const ecrRegistryBackend: RegistryLoginBackend = createEcrRegistryBackend();
|
|
178
92
|
|
|
179
93
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* `ecs-update-service` reference via `imageRef: "@Publish.digest"`. No
|
|
185
|
-
* rollback: an already-pushed, still-valid image in the registry is not
|
|
186
|
-
* itself a problem to compensate (immutable, content-addressed, and simply
|
|
187
|
-
* unreferenced if a later step fails) — the opt-out this capability takes.
|
|
188
|
-
*
|
|
189
|
-
* #610: when `input.sbom` and/or `input.componentBom` are supplied, also
|
|
190
|
-
* attaches them as OCI referrers on the pushed digest via `oras attach` (see
|
|
191
|
-
* `attachReferrers` above) — best-effort, guarded on `oras` availability,
|
|
192
|
-
* surfaced on the output's `referrerAttach` rather than failing the publish.
|
|
94
|
+
* `publish-image` with ECR as its only login backend: core's verb, bound to
|
|
95
|
+
* this executor. Kept so aws-only callers and tests build it the way they
|
|
96
|
+
* always did; behaviour is unchanged (load, tag, ECR login, push, optional
|
|
97
|
+
* referrer attach).
|
|
193
98
|
*/
|
|
194
99
|
export function createPublishImageCapability(
|
|
195
100
|
executor: CloudExecutor = defaultCloudExecutor(),
|
|
196
101
|
processRunner: ProcessRunner = defaultProcessRunner(),
|
|
197
102
|
): PublishImageBackend {
|
|
198
|
-
return {
|
|
199
|
-
kind: "publish-image",
|
|
200
|
-
async run(_ctx, input) {
|
|
201
|
-
if (!input.to) throw new Error(`publish-image "${input.from}": "to" (destination registry) is required`);
|
|
202
|
-
// `docker load` reports the archived image's local reference — a name:tag
|
|
203
|
-
// for a tagged build, or an image id. It's a valid `docker tag` source but
|
|
204
|
-
// never a registry digest, so the promoted digest is read back from the
|
|
205
|
-
// push below, not constructed from this.
|
|
206
|
-
const { digest: localRef } = await executor.docker.load({ inFile: archiveRelativePath(input.from) });
|
|
207
|
-
const repo = input.to.replace(/\/+$/, "");
|
|
208
|
-
// Publish under a tag: docker can neither tag nor push a digest-qualified
|
|
209
|
-
// reference (`repo@sha256:...`), so the transport is a tag and the stable
|
|
210
|
-
// identifier is the digest the push returns.
|
|
211
|
-
const pushRef = `${repo}:latest`;
|
|
212
|
-
await executor.docker.tag({ source: localRef, target: pushRef });
|
|
213
|
-
const registry = repo.split("/")[0]!;
|
|
214
|
-
await executor.ecr.login(registry);
|
|
215
|
-
const { digest: pushed } = await executor.docker.push({ image: pushRef });
|
|
216
|
-
// Normalize to a bare `sha256:...`, whether the client returned that or a
|
|
217
|
-
// full `repo@sha256:...` RepoDigest.
|
|
218
|
-
const digest = pushed.includes("@") ? pushed.slice(pushed.indexOf("@") + 1) : pushed;
|
|
219
|
-
for (const tag of input.tags ?? []) {
|
|
220
|
-
const tagged = `${repo}:${tag}`;
|
|
221
|
-
await executor.docker.tag({ source: localRef, target: tagged });
|
|
222
|
-
await executor.docker.push({ image: tagged });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const referrerAttach = await attachReferrers(processRunner, `${repo}@${digest}`, input);
|
|
226
|
-
return { digest, uri: `${repo}@${digest}`, ...(referrerAttach ? { referrerAttach } : {}) };
|
|
227
|
-
},
|
|
228
|
-
};
|
|
103
|
+
return createCorePublishImage({ docker: executor.docker, processRunner, backends: [createEcrRegistryBackend(executor)] });
|
|
229
104
|
}
|
|
230
105
|
|
|
231
|
-
/**
|
|
106
|
+
/** `publish-image` bound to the real executor; the registered verb is core's, with this lexicon's ECR backend injected. */
|
|
232
107
|
export const publishImageCapability: PublishImageBackend = createPublishImageCapability();
|
|
233
108
|
|
|
234
109
|
// ── load-image-on-host (host backend) ───────────────────────────────────────
|
|
@@ -309,48 +184,41 @@ export function selectPublishBackend(
|
|
|
309
184
|
return backend;
|
|
310
185
|
}
|
|
311
186
|
|
|
312
|
-
// ── publish-asset / publish-artifact
|
|
187
|
+
// ── publish-asset / publish-artifact (the verb is core's, #4408) ───────────
|
|
313
188
|
|
|
314
|
-
export
|
|
315
|
-
/** Path of the artifact inside the build archive (e.g. a jar or zip). */
|
|
316
|
-
from: string;
|
|
317
|
-
/** Destination (e.g. `$env.s3` resolved by the orchestrator). */
|
|
318
|
-
to: string;
|
|
319
|
-
}
|
|
189
|
+
export type { PublishArtifactInput, PublishArtifactOutput } from "@intentius/chant/components/verbs/publish-artifact";
|
|
320
190
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
191
|
+
/**
|
|
192
|
+
* The aws lexicon's storage backend for core's `publish-artifact`: `aws s3 cp`
|
|
193
|
+
* (endpoint-aware through the `CloudExecutor`). Core owns the digest and the
|
|
194
|
+
* object URI; this only uploads.
|
|
195
|
+
*/
|
|
196
|
+
export function createS3StorageBackend(executor?: CloudExecutor): ArtifactStorageBackend {
|
|
197
|
+
return {
|
|
198
|
+
name: "s3",
|
|
199
|
+
matches: (uri) => uri.startsWith("s3://"),
|
|
200
|
+
// The real executor is built on first upload, so loading the plugin shells out to nothing.
|
|
201
|
+
upload: (args) => (executor ?? defaultCloudExecutor()).s3.cp(args),
|
|
202
|
+
};
|
|
326
203
|
}
|
|
327
204
|
|
|
328
|
-
/**
|
|
205
|
+
/** The S3 backend the aws plugin contributes, over the real `CloudExecutor`. */
|
|
206
|
+
export const s3StorageBackend: ArtifactStorageBackend = createS3StorageBackend();
|
|
207
|
+
|
|
329
208
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* the exact bytes uploaded.
|
|
209
|
+
* `publish-artifact` with S3 as its only storage backend: core's verb, bound
|
|
210
|
+
* to this executor. Kept so aws-only callers and tests build it the way they
|
|
211
|
+
* always did; behaviour is unchanged (upload, return uri + content digest).
|
|
334
212
|
*/
|
|
335
213
|
export function createPublishArtifactCapability(
|
|
336
214
|
executor: CloudExecutor = defaultCloudExecutor(),
|
|
337
215
|
): Capability<PublishArtifactInput, PublishArtifactOutput> {
|
|
338
|
-
return {
|
|
339
|
-
kind: "publish-artifact",
|
|
340
|
-
async run(_ctx, input) {
|
|
341
|
-
const localPath = archiveRelativePath(input.from);
|
|
342
|
-
const digest = `sha256:${createHash("sha256").update(readFileSync(localPath)).digest("hex")}`;
|
|
343
|
-
const basename = localPath.split("/").pop() ?? localPath;
|
|
344
|
-
const uri = input.to.endsWith("/") ? `${input.to}${basename}` : input.to;
|
|
345
|
-
await executor.s3.cp({ from: localPath, to: uri });
|
|
346
|
-
return { uri, digest };
|
|
347
|
-
},
|
|
348
|
-
};
|
|
216
|
+
return createCorePublishArtifact({ backends: [createS3StorageBackend(executor)] });
|
|
349
217
|
}
|
|
350
218
|
|
|
351
|
-
/** Default `publish-artifact
|
|
219
|
+
/** Default `publish-artifact`, bound to the real executor; the registered verb is core's, with this lexicon's S3 backend injected. */
|
|
352
220
|
export const publishArtifactCapability: Capability<PublishArtifactInput, PublishArtifactOutput> =
|
|
353
221
|
createPublishArtifactCapability();
|
|
354
222
|
|
|
355
|
-
/** Alias for `publish-artifact
|
|
223
|
+
/** Alias for `publish-artifact`: same capability, the docs/epic use both names for the same verb. */
|
|
356
224
|
export const publishAssetCapability = publishArtifactCapability;
|
package/src/import/parser.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { canDescribe, describeOwnProperties, stampRegion } from "./properties";
|
|
|
36
36
|
import { stampProviderDefaults } from "./defaults";
|
|
37
37
|
import { resolveTemplateAttrs } from "./live-attrs";
|
|
38
38
|
import { CFParser } from "./import/parser";
|
|
39
|
+
import { lastMemberReport, readCloudFormationMember, setLastMemberReport } from "./workspace-member";
|
|
39
40
|
import { CFGenerator } from "./import/generator";
|
|
40
41
|
import { parseStackTemplate } from "./import/live-export";
|
|
41
42
|
import { awsCompletions } from "./lsp/completions";
|
|
@@ -65,6 +66,21 @@ const OWNERSHIP_UNRESOLVED_NOTE =
|
|
|
65
66
|
export const awsPlugin: LexiconPlugin = {
|
|
66
67
|
behaviourKinds: awsBehaviourKinds,
|
|
67
68
|
name: "aws",
|
|
69
|
+
// A cloudformation workspace member's reader project (#3179): the template's
|
|
70
|
+
// resources as nodes, its Ref, GetAtt, Sub and DependsOn as edges. See
|
|
71
|
+
// ./workspace-member.ts.
|
|
72
|
+
async buildRoots(ctx) {
|
|
73
|
+
setLastMemberReport(undefined);
|
|
74
|
+
const member = (ctx.config as { aws?: { member?: { kind?: "cloudformation" | "cdk"; dir: string; label?: string } } }).aws?.member;
|
|
75
|
+
if (!member) return { entities: new Map() };
|
|
76
|
+
const read = readCloudFormationMember({ member });
|
|
77
|
+
setLastMemberReport(read.report);
|
|
78
|
+
return { entities: read.entities, warnings: read.warnings };
|
|
79
|
+
},
|
|
80
|
+
graphMeta() {
|
|
81
|
+
const report = lastMemberReport();
|
|
82
|
+
return report ? { awsMember: report } : undefined;
|
|
83
|
+
},
|
|
68
84
|
// The thin read's marker channel is the STACK's own tags (#1998), read off
|
|
69
85
|
// the DescribeStacks call it already makes for the outputs.
|
|
70
86
|
// describe-stack-resources carries no per-resource tags, so the stack is the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"app": "npx ts-node bin/app.ts"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Resources": {
|
|
3
|
+
"QueueABC123": {
|
|
4
|
+
"Type": "AWS::SQS::Queue",
|
|
5
|
+
"Properties": { "VisibilityTimeout": 30 }
|
|
6
|
+
},
|
|
7
|
+
"HandlerRoleDEF456": {
|
|
8
|
+
"Type": "AWS::IAM::Role",
|
|
9
|
+
"Properties": { "AssumeRolePolicyDocument": { "Statement": [] } }
|
|
10
|
+
},
|
|
11
|
+
"HandlerGHI789": {
|
|
12
|
+
"Type": "AWS::Lambda::Function",
|
|
13
|
+
"Properties": {
|
|
14
|
+
"Role": { "Fn::GetAtt": ["HandlerRoleDEF456", "Arn"] },
|
|
15
|
+
"Environment": { "Variables": { "QUEUE_URL": { "Ref": "QueueABC123" } } }
|
|
16
|
+
},
|
|
17
|
+
"DependsOn": ["HandlerRoleDEF456"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version": "36.0.0", "artifacts": {}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"app": "npx ts-node bin/app.ts"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
not a template
|