@intentius/behold 0.8.0 → 0.9.1
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/AGENTS.md +85 -0
- package/README.md +124 -2
- package/demos.json +17 -1
- package/dist/cli.js +2553 -315
- package/example-argo-estate/README.md +43 -18
- package/example-argo-estate/app-a/chant.config.ts +10 -2
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/chant.config.ts +11 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/chant.config.ts +20 -5
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +20 -20
- package/example-carve/README.md +221 -0
- package/example-carve/app/chant.config.ts +6 -0
- package/example-carve/app/package-lock.json +1075 -0
- package/example-carve/app/package.json +13 -0
- package/example-carve/app/src/carved.ts +30 -0
- package/example-carve/app/tsconfig.json +1 -0
- package/example-carve/carve-report.json +962 -0
- package/example-carve/legacy-tf/cdn.tf +23 -0
- package/example-carve/legacy-tf/compute.tf +63 -0
- package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
- package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
- package/example-carve/legacy-tf/naming.tf +10 -0
- package/example-carve/legacy-tf/network.tf +119 -0
- package/example-carve/legacy-tf/observability.tf +18 -0
- package/example-carve/legacy-tf/outputs.tf +21 -0
- package/example-carve/legacy-tf/storage.tf +33 -0
- package/example-carve/legacy-tf/terraform.tfstate +602 -0
- package/example-carve/legacy-tf/versions.tf +40 -0
- package/example-flux-estate/README.md +9 -4
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-a/src/app.ts +2 -1
- package/example-flux-estate/app-b/chant.config.ts +4 -3
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/app-b/src/app.ts +5 -3
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/control-plane/src/flux.ts +4 -2
- package/example-flux-estate/package-lock.json +20 -20
- package/example-k8s/package-lock.json +21 -21
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +8 -6
- package/web/app.js +714 -57
- package/web/carve-steps.js +621 -0
- package/web/carve-steps.test.js +248 -0
- package/web/demos.js +71 -0
- package/web/demos.test.js +83 -0
- package/web/index.html +93 -1
- package/web/json-view.js +334 -0
- package/web/json-view.test.js +218 -0
- package/web/layout-store.js +164 -4
- package/web/layout-store.test.js +226 -1
- package/web/panel.js +28 -0
- package/web/theme.js +57 -1
|
@@ -14,25 +14,31 @@ Three chant projects served as one composed estate, the Argo mirror of
|
|
|
14
14
|
npx @intentius/behold demo argo-estate
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
The demo path is **declared only** — no cluster, no Argo install, no Docker. It
|
|
18
|
+
copies out, `npm install`s, and serves. (There is a live lane, but it is an
|
|
19
|
+
acceptance run rather than part of the demo: see "Live" below.) What to look
|
|
20
|
+
at:
|
|
19
21
|
|
|
20
22
|
- The **estate**: per-project boundary boxes, and the `project` edges joining
|
|
21
23
|
both Applications to the AppProject they name (behold#222) — the join chant
|
|
22
24
|
lints as ARGO002, so an estate whose lint passes cannot be drawn wrong.
|
|
23
25
|
The entity graph carries them at the attributes tier (`?detail=3`).
|
|
24
|
-
- The **logical lens**
|
|
25
|
-
|
|
26
|
-
`namespace
|
|
27
|
-
`Namespace` object anywhere.
|
|
28
|
-
`spec.destination.namespace
|
|
29
|
-
namespace
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
- The **logical lens** (zoom: logical): cluster ⊃ `namespace app-a` and
|
|
27
|
+
`namespace app-b`, each holding that app's Deployment + Service, plus
|
|
28
|
+
`namespace argocd` holding both Applications and the AppProject — drawn
|
|
29
|
+
even though the control plane declares no `Namespace` object anywhere. The
|
|
30
|
+
app namespaces come from each Application's `spec.destination.namespace`; a
|
|
31
|
+
namespace Argo will own objects in is a namespace the estate is committed
|
|
32
|
+
to, and behold#224 taught the composed estate to draw that projection.
|
|
33
|
+
Serve `control-plane` alone (`behold serve control-plane`, zoom: logical)
|
|
34
|
+
and the same boxes appear empty — the destination harvest still names them,
|
|
35
|
+
but there is nothing from app-a/app-b in the graph to fill them with.
|
|
36
|
+
|
|
37
|
+
That alone-vs-composed split is the Argo/Flux difference worth noticing:
|
|
38
|
+
flux-estate's control plane must declare the app namespaces itself (a
|
|
39
|
+
Kustomization's `targetNamespace` must already exist), while here
|
|
40
|
+
`CreateNamespace=true` means Argo makes them — so the box exists on the
|
|
41
|
+
strength of the destination alone.
|
|
36
42
|
|
|
37
43
|
Argo's ordering is the `argocd.argoproj.io/sync-wave` annotation, so unlike
|
|
38
44
|
Flux's `dependsOn` (behold#223) there is no edge to draw for it: app-a is wave
|
|
@@ -40,7 +46,26 @@ Flux's `dependsOn` (behold#223) there is no edge to draw for it: app-a is wave
|
|
|
40
46
|
|
|
41
47
|
The app `manifests/` are committed `chant build` output — the path each
|
|
42
48
|
Application syncs. Changing an app's source means a rebuild (`npm run build` in
|
|
43
|
-
the app)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
the app), and the rebuilt manifests have to be on `main` before Argo can sync
|
|
50
|
+
them. `chant build` in the control plane warns ARGO005 on both Applications:
|
|
51
|
+
`source.path` is repo-relative (that's what Argo resolves it against), not
|
|
52
|
+
relative to the build root, so it doesn't resolve locally. Fine here, and the
|
|
53
|
+
check's own message says so.
|
|
54
|
+
|
|
55
|
+
## Live
|
|
56
|
+
|
|
57
|
+
Nothing above needs a cluster, but the estate as committed does reconcile
|
|
58
|
+
against a real one — the Applications point at this repo on `main` and the app
|
|
59
|
+
`manifests/` are already there, so an Argo CD install has everything it needs.
|
|
60
|
+
`just e2e-argo-estate` (behold#269) is that run: a scratch k3d cluster, Argo
|
|
61
|
+
from the pinned `core-install` manifest, the control plane applied once, and
|
|
62
|
+
then the composed estate served against the result — the destination-namespace
|
|
63
|
+
boxes filled with live cards, #238's health verdicts read off each
|
|
64
|
+
Application's health/sync pair, and the unhappy arm (an Application pointed at
|
|
65
|
+
a path that does not exist, then restored). It creates and deletes its own
|
|
66
|
+
cluster and touches no other context.
|
|
67
|
+
|
|
68
|
+
Each member's `chant.config.ts` therefore binds a `local` profile to the
|
|
69
|
+
context `k3d-behold-argo-demo`, which is what that run aliases its scratch
|
|
70
|
+
cluster to. Serving without `--env` — what `behold demo argo-estate` does —
|
|
71
|
+
reads none of it.
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { ChantConfig } from "@intentius/chant";
|
|
2
|
+
import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
|
|
2
3
|
|
|
3
4
|
// App project A. Argo syncs its committed manifests/ (chant build output) from
|
|
4
|
-
// the repo; this project applies nothing itself.
|
|
5
|
-
//
|
|
5
|
+
// the repo; this project applies nothing itself. behold serves it as part of
|
|
6
|
+
// the estate, and the live overlay reads the cluster through the same binding
|
|
7
|
+
// as every other member — see ../control-plane/chant.config.ts.
|
|
6
8
|
export default {
|
|
7
9
|
lexicons: ["k8s"],
|
|
8
10
|
sourceDir: "src",
|
|
11
|
+
environments: ["local"],
|
|
9
12
|
ownership: { stack: "argo-app-a" },
|
|
13
|
+
k8s: {
|
|
14
|
+
profiles: {
|
|
15
|
+
local: { context: "k3d-behold-argo-demo" },
|
|
16
|
+
},
|
|
17
|
+
} satisfies K8sChantConfig,
|
|
10
18
|
} satisfies ChantConfig;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { ChantConfig } from "@intentius/chant";
|
|
2
|
+
import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
|
|
2
3
|
|
|
3
|
-
// App project B — same shape as app-a, second sync wave.
|
|
4
|
-
//
|
|
4
|
+
// App project B — same shape as app-a, second sync wave. Unlike flux-estate's
|
|
5
|
+
// app-b this one declares its own namespace on every object (Argo has no
|
|
6
|
+
// targetNamespace to stamp one at apply time), so a live read resolves it
|
|
7
|
+
// straight away. See ../control-plane/chant.config.ts for the binding.
|
|
5
8
|
export default {
|
|
6
9
|
lexicons: ["k8s"],
|
|
7
10
|
sourceDir: "src",
|
|
11
|
+
environments: ["local"],
|
|
8
12
|
ownership: { stack: "argo-app-b" },
|
|
13
|
+
k8s: {
|
|
14
|
+
profiles: {
|
|
15
|
+
local: { context: "k3d-behold-argo-demo" },
|
|
16
|
+
},
|
|
17
|
+
} satisfies K8sChantConfig,
|
|
9
18
|
} satisfies ChantConfig;
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import type { ChantConfig } from "@intentius/chant";
|
|
2
|
+
import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
|
|
2
3
|
|
|
3
4
|
// The estate's control plane: it declares the Argo machinery (an AppProject +
|
|
4
|
-
// one Application per app), and Argo's controller deploys the app
|
|
5
|
-
// this project never applies a workload itself.
|
|
5
|
+
// one Application per app), and Argo's application controller deploys the app
|
|
6
|
+
// projects — this project never applies a workload itself.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// `npx @intentius/behold demo argo-estate` still needs no cluster: it serves
|
|
9
|
+
// the source graph with no `--env`, and nothing below is read on that path.
|
|
10
|
+
// The `local` profile exists for the live lane (behold#269,
|
|
11
|
+
// `just e2e-argo-estate`), which stands up a scratch k3d cluster and aliases
|
|
12
|
+
// its context to the name pinned here — so a live read resolves the DECLARED
|
|
13
|
+
// binding and can never observe whatever kubectl context happens to be
|
|
14
|
+
// ambient (chant#1100).
|
|
15
|
+
//
|
|
16
|
+
// Deliberately no `ownership.env`: the app members' committed manifests/ are
|
|
17
|
+
// what Argo syncs from `main`, so a label that changes their build output
|
|
18
|
+
// would have to be merged before any live run could agree with the cluster.
|
|
10
19
|
export default {
|
|
11
20
|
lexicons: ["k8s"],
|
|
12
21
|
sourceDir: "src",
|
|
22
|
+
environments: ["local"],
|
|
13
23
|
ownership: { stack: "argo-control-plane" },
|
|
24
|
+
k8s: {
|
|
25
|
+
profiles: {
|
|
26
|
+
local: { context: "k3d-behold-argo-demo" },
|
|
27
|
+
},
|
|
28
|
+
} satisfies K8sChantConfig,
|
|
14
29
|
} satisfies ChantConfig;
|
|
@@ -14,22 +14,22 @@
|
|
|
14
14
|
"app-a": {
|
|
15
15
|
"name": "behold-example-argo-app-a",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@intentius/chant": "^0.44.
|
|
18
|
-
"@intentius/chant-lexicon-k8s": "^0.44.
|
|
17
|
+
"@intentius/chant": "^0.44.7",
|
|
18
|
+
"@intentius/chant-lexicon-k8s": "^0.44.7"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"app-b": {
|
|
22
22
|
"name": "behold-example-argo-app-b",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@intentius/chant": "^0.44.
|
|
25
|
-
"@intentius/chant-lexicon-k8s": "^0.44.
|
|
24
|
+
"@intentius/chant": "^0.44.7",
|
|
25
|
+
"@intentius/chant-lexicon-k8s": "^0.44.7"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"control-plane": {
|
|
29
29
|
"name": "behold-example-argo-control-plane",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@intentius/chant": "^0.44.
|
|
32
|
-
"@intentius/chant-lexicon-k8s": "^0.44.
|
|
31
|
+
"@intentius/chant": "^0.44.7",
|
|
32
|
+
"@intentius/chant-lexicon-k8s": "^0.44.7"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"node_modules/@dagrejs/dagre": {
|
|
@@ -464,9 +464,9 @@
|
|
|
464
464
|
}
|
|
465
465
|
},
|
|
466
466
|
"node_modules/@intentius/chant": {
|
|
467
|
-
"version": "0.44.
|
|
468
|
-
"resolved": "https://registry.npmjs.org/@intentius/chant/-/chant-0.44.
|
|
469
|
-
"integrity": "sha512-
|
|
467
|
+
"version": "0.44.7",
|
|
468
|
+
"resolved": "https://registry.npmjs.org/@intentius/chant/-/chant-0.44.7.tgz",
|
|
469
|
+
"integrity": "sha512-etCchkfbWFHJ+XnTYVVn9GT4/gZi/O8SVphDXHeuPOfK5VvRxo0unveIUEsuRoHGHgzN9Y+aj0Jk15xhIE7Gzg==",
|
|
470
470
|
"license": "Apache-2.0",
|
|
471
471
|
"dependencies": {
|
|
472
472
|
"@dagrejs/dagre": "^3.0.0",
|
|
@@ -482,9 +482,9 @@
|
|
|
482
482
|
}
|
|
483
483
|
},
|
|
484
484
|
"node_modules/@intentius/chant-k8s-client": {
|
|
485
|
-
"version": "0.44.
|
|
486
|
-
"resolved": "https://registry.npmjs.org/@intentius/chant-k8s-client/-/chant-k8s-client-0.44.
|
|
487
|
-
"integrity": "sha512-
|
|
485
|
+
"version": "0.44.7",
|
|
486
|
+
"resolved": "https://registry.npmjs.org/@intentius/chant-k8s-client/-/chant-k8s-client-0.44.7.tgz",
|
|
487
|
+
"integrity": "sha512-G9VVki/qHZ+hbKrxDDnVVhfeZWoHgozLA7tjcY/lsa/fDIwI0xMJnAN/DJNcQLBpRscXm3y3Ki/PNQ9SxshmLA==",
|
|
488
488
|
"license": "Apache-2.0",
|
|
489
489
|
"optional": true,
|
|
490
490
|
"dependencies": {
|
|
@@ -492,19 +492,19 @@
|
|
|
492
492
|
}
|
|
493
493
|
},
|
|
494
494
|
"node_modules/@intentius/chant-lexicon-k8s": {
|
|
495
|
-
"version": "0.44.
|
|
496
|
-
"resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-k8s/-/chant-lexicon-k8s-0.44.
|
|
497
|
-
"integrity": "sha512-
|
|
495
|
+
"version": "0.44.7",
|
|
496
|
+
"resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-k8s/-/chant-lexicon-k8s-0.44.7.tgz",
|
|
497
|
+
"integrity": "sha512-h8QXOYoxjsAeA1w8koLQlZqOuSQcvWpEHKmwlf0AKPgwuVRiapwJI+UtuoBwL/AOrBKDh6Z5pW3zbo6vDltGvA==",
|
|
498
498
|
"license": "Apache-2.0",
|
|
499
499
|
"dependencies": {
|
|
500
500
|
"@types/js-yaml": "^4.0.9",
|
|
501
501
|
"js-yaml": "^4.1.1"
|
|
502
502
|
},
|
|
503
503
|
"optionalDependencies": {
|
|
504
|
-
"@intentius/chant-k8s-client": "^0.44.
|
|
504
|
+
"@intentius/chant-k8s-client": "^0.44.7"
|
|
505
505
|
},
|
|
506
506
|
"peerDependencies": {
|
|
507
|
-
"@intentius/chant": "^0.44.
|
|
507
|
+
"@intentius/chant": "^0.44.7",
|
|
508
508
|
"typescript": "^5.9.3",
|
|
509
509
|
"zod": "^4.3.6"
|
|
510
510
|
}
|
|
@@ -1071,9 +1071,9 @@
|
|
|
1071
1071
|
}
|
|
1072
1072
|
},
|
|
1073
1073
|
"node_modules/ip-address": {
|
|
1074
|
-
"version": "10.
|
|
1075
|
-
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.
|
|
1076
|
-
"integrity": "sha512-
|
|
1074
|
+
"version": "10.5.0",
|
|
1075
|
+
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
|
|
1076
|
+
"integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
|
|
1077
1077
|
"license": "MIT",
|
|
1078
1078
|
"optional": true,
|
|
1079
1079
|
"engines": {
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# example-carve — the mixed estate the peeling demo runs on
|
|
2
|
+
|
|
3
|
+
A migration caught half-done. `app/` is chant; `legacy-tf/` is Terraform; both
|
|
4
|
+
describe one AWS account, and the walkthrough carves one more resource across
|
|
5
|
+
the line while the camera is running.
|
|
6
|
+
|
|
7
|
+
Run it:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
behold demo carve # copies this directory, installs, advises, serves
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The boot does three things before a port opens: `npm install` in `app/` (whose
|
|
14
|
+
chant every step of the walkthrough shells), `@cdktf/hcl2json` into the copy's
|
|
15
|
+
root `node_modules` (chant lazy-loads the HCL parser from its own install
|
|
16
|
+
upward, so `<copy>/node_modules` is where it resolves — not beside the `.tf`
|
|
17
|
+
files), and `chant carve advise --report` over the copy's own Terraform. If any
|
|
18
|
+
of that fails, the committed `carve-report.json` is served instead and the
|
|
19
|
+
reason is on screen; a blank graph is the one outcome that's never allowed.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
example-carve/
|
|
23
|
+
app/ a small chant project — the pieces already carved
|
|
24
|
+
carveout/ where Emit and Bridge write, in a copy (gitignored)
|
|
25
|
+
legacy-tf/ the Terraform half, still Terraform-owned
|
|
26
|
+
terraform.tfstate synthetic state (fake account, fake ARNs)
|
|
27
|
+
floci-override.tf.disabled provider endpoints for the --live tier, inert
|
|
28
|
+
modules/cdn/ a local module, so module.cdn resolves offline
|
|
29
|
+
carve-report.json the committed `carve advise` output
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`carveout/` sits INSIDE `app/` rather than beside it, and that is load-bearing:
|
|
33
|
+
the emitted source imports `@intentius/chant-lexicon-aws`, and Node resolves
|
|
34
|
+
that from the file's own directory upward. From `app/carveout/src/assets.ts` it
|
|
35
|
+
reaches `app/node_modules`; from a sibling `carveout/` it would reach nothing,
|
|
36
|
+
and the Emit step's `chant lint` would fail on an install problem rather than
|
|
37
|
+
on the source.
|
|
38
|
+
|
|
39
|
+
## The estate is mixed from the first frame
|
|
40
|
+
|
|
41
|
+
`app/src/carved.ts` holds a CloudWatch log group and an SSM parameter that came
|
|
42
|
+
out of `legacy-tf/` last month. The source says so, and `legacy-tf/cdn.tf` still
|
|
43
|
+
carries the other half of that carve: a `data "aws_ssm_parameter"` block reading
|
|
44
|
+
back the value chant now owns. That is the data-source rewrite `carve bridge`
|
|
45
|
+
generates, sitting in the estate a month later and working.
|
|
46
|
+
|
|
47
|
+
So the estate view opens on a chant box beside a Terraform box. The morph at the
|
|
48
|
+
end of the walkthrough has somewhere to land.
|
|
49
|
+
|
|
50
|
+
## Bands
|
|
51
|
+
|
|
52
|
+
Real output, from `chant carve advise` at chant 0.44.7. Reproduce it with:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install -D @cdktf/hcl2json # once, anywhere on your PATH resolution
|
|
56
|
+
chant carve advise --from example-carve/legacy-tf \
|
|
57
|
+
--state example-carve/legacy-tf/terraform.tfstate
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| score | address | band | why | role in the script |
|
|
61
|
+
|---|---|---|---|---|
|
|
62
|
+
| 100 | `aws_cloudwatch_log_group.worker` | clean leaf | clean 1:1 native map, no boundary edges | the free first move: named, not carved |
|
|
63
|
+
| 84 | `aws_s3_bucket.assets` | clean leaf | 1 inbound, 1 output | **the star** — the resource we carve on camera |
|
|
64
|
+
| 71 | `module.cdn` | carvable w/ edits | 1 outbound, tier 2, data present | one module, so module scoring appears |
|
|
65
|
+
| 69 | `aws_iam_role.api` | carvable w/ edits | 1 inbound, 1 outbound, tier 2 | what an inbound edge costs |
|
|
66
|
+
| 69 | `aws_vpc_endpoint.ssm` | carvable w/ edits | 4 outbound, tier 2 | outbound edges are cheap, but they add up |
|
|
67
|
+
| 61 | `aws_lambda_function.api` | carvable w/ edits | 1 output, 5 outbound, tier 2 | the honest yellow: next month, not today |
|
|
68
|
+
| 43 | `aws_security_group.lambda` | leave in Terraform | 2 inbound, 2 outbound, tier 2, data present | |
|
|
69
|
+
| 42 | `aws_subnet.private_a` | leave in Terraform | 3 inbound, 1 output, 2 outbound, data present | |
|
|
70
|
+
| 42 | `aws_subnet.private_b` | leave in Terraform | 3 inbound, 1 output, 2 outbound, data present | |
|
|
71
|
+
| 32 | `aws_vpc.main` | leave in Terraform | 5 inbound, 1 output, 1 outbound | the grey anchor: this stays, and that's fine |
|
|
72
|
+
| 0 | `aws_network_acl.private` | leave in Terraform | no known native mapping | |
|
|
73
|
+
| 0 | `random_pet.suffix` | leave in Terraform | no known native mapping | the long tail, stated plainly |
|
|
74
|
+
|
|
75
|
+
Bands: 80-100 carve now, 50-79 carve with boundary edits, 0-49 leave in Terraform.
|
|
76
|
+
|
|
77
|
+
The scoring model is `100 - 12*inbound - 4*outbound - 4*outputs - 15*(tier-1) -
|
|
78
|
+
10*dynamic - 3*(instances-1)`, clamped, with unmapped types pinned at 0. The
|
|
79
|
+
`outputs` term is chant#1638 (chant 0.44.7): an `output` block that reads a
|
|
80
|
+
resource is a boundary edge too now, `bridge: "tf-output-rewrite"`, because
|
|
81
|
+
carving that resource means rewriting the output the same way carving a
|
|
82
|
+
resource with an inbound reference means adding a data source. Every number
|
|
83
|
+
above is that arithmetic on the edges in `legacy-tf/`, which is why the estate
|
|
84
|
+
is shaped the way it is:
|
|
85
|
+
|
|
86
|
+
- The bucket has one inbound edge — `aws_lambda_function.api`'s
|
|
87
|
+
`ASSETS_BUCKET` environment variable — and, since `outputs.tf`'s
|
|
88
|
+
`assets_bucket` reads it too, one output edge. Two edges, two rewrites, 84.
|
|
89
|
+
`assets_bucket` is deliberate: a downstream consumer of this state reads the
|
|
90
|
+
bucket name today, so the carve has a second thing to bridge, not just the
|
|
91
|
+
lambda's env var.
|
|
92
|
+
- `aws_s3_bucket_versioning.assets` and `aws_s3_bucket_public_access_block.assets`
|
|
93
|
+
share the bucket's name, so they fold into its carve set. They are not ranked
|
|
94
|
+
separately and their edges to the bucket are not counted against it.
|
|
95
|
+
- Five resources point at the VPC, and its existing `vpc_id` output reads it
|
|
96
|
+
too. Carving it would mean patching all five survivors and rewriting the
|
|
97
|
+
output, and 32 says so.
|
|
98
|
+
- The subnets and the security group each read a data source, which costs 10.
|
|
99
|
+
The subnets also feed `private_subnet_ids`, an existing output, which costs
|
|
100
|
+
4 more apiece and drops them under the security group's 43 — the one place
|
|
101
|
+
the outputs term reorders the table, not just re-scores it.
|
|
102
|
+
|
|
103
|
+
No resource uses `count` or `for_each`, so the instance-count penalty is zero
|
|
104
|
+
and `--state` produces byte-identical scores to a `.tf`-only parse. The bands do
|
|
105
|
+
not move depending on which way the walkthrough invokes the advisor.
|
|
106
|
+
|
|
107
|
+
### carve-report.json
|
|
108
|
+
|
|
109
|
+
`carve-report.json` is the `--report` output of exactly the command above,
|
|
110
|
+
committed so the walkthrough and #252's carve lens have an artifact to read
|
|
111
|
+
without shelling anything. JSON carries no comments, so this is its header:
|
|
112
|
+
regenerate it, unchanged, with
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
chant carve advise --from example-carve/legacy-tf \
|
|
116
|
+
--state example-carve/legacy-tf/terraform.tfstate \
|
|
117
|
+
--report example-carve/carve-report.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
from the repository root, so the `from` field stays a relative path.
|
|
121
|
+
|
|
122
|
+
`app/` pins chant ^0.44.7 and that floor is load-bearing, not housekeeping, in
|
|
123
|
+
two layers. The advisor only publishes a top-level `version: 1` and
|
|
124
|
+
per-resource `boundary` edge lists from 0.44.6 (chant#1636) — run the same
|
|
125
|
+
command on 0.44.4 and every score is identical but no resource carries a
|
|
126
|
+
`boundary` field at all, so the Pick step falls back to naming inbound/outbound
|
|
127
|
+
COUNTS instead of naming the edges the carve severs. 0.44.7 adds a second
|
|
128
|
+
layer on top: `output` blocks read as boundary edges (chant#1638), so the
|
|
129
|
+
bucket scores 84 instead of 88 and the report's `patchOnCarve` list names
|
|
130
|
+
`output.assets_bucket` alongside `aws_lambda_function.api`; and `carve emit`'s
|
|
131
|
+
fold is applied, not just reported (chant#1637), so the emitted bucket actually
|
|
132
|
+
carries `VersioningConfiguration` and `PublicAccessBlockConfiguration` instead
|
|
133
|
+
of leaving them in an "unmapped attributes" comment. The committed report above
|
|
134
|
+
is byte-identical to what 0.44.7 regenerates, which is why the demo's fallback
|
|
135
|
+
path and its fresh-run path show the same picture.
|
|
136
|
+
|
|
137
|
+
## The six beats
|
|
138
|
+
|
|
139
|
+
These are the six steps on the panel's Carve tab. behold runs beats 4 and 5
|
|
140
|
+
itself, into `app/carveout/` in the copy; beat 6 is copy buttons, on purpose.
|
|
141
|
+
|
|
142
|
+
1. **The green star.** The estate view, banded. Two resources are green; the
|
|
143
|
+
100 is a log group nobody will miss, so the eye lands on the 84 next to it.
|
|
144
|
+
2. **The arithmetic.** Open `aws_s3_bucket.assets`: 100 minus 12 for the
|
|
145
|
+
inbound edge, minus 4 more for the output block that reads it. The score is
|
|
146
|
+
not a vibe, it is a subtraction, and the inspect pane shows the terms.
|
|
147
|
+
3. **The cut edge.** Highlight the two dependencies the carve severs:
|
|
148
|
+
`aws_lambda_function.api` reads the bucket name, and the `assets_bucket`
|
|
149
|
+
output reads it too. Two one-line rewrites, not one.
|
|
150
|
+
4. **Emit.** `carve emit --from legacy-tf --state legacy-tf/terraform.tfstate
|
|
151
|
+
--select aws_s3_bucket.assets` adopts the bucket from state into typed chant
|
|
152
|
+
source, folds both sub-resources into the carve set AS native props
|
|
153
|
+
(chant#1637's fold, applied), and writes a boundary report. Nothing is
|
|
154
|
+
applied, nothing is destroyed, and `chant lint` on the result exits clean.
|
|
155
|
+
5. **The bridge.** One `data "aws_s3_bucket" "assets"` block in the surviving
|
|
156
|
+
Terraform; `aws_lambda_function.api` and the `assets_bucket` output both
|
|
157
|
+
read from that instead. The surviving plan is whole again.
|
|
158
|
+
6. **The handoff.** Two commands: `terraform state rm aws_s3_bucket.assets`,
|
|
159
|
+
then chant takes the observe position. `terraform plan` shows no destroy.
|
|
160
|
+
|
|
161
|
+
Then the box slides out of the Terraform half and into `app/`, beside the log
|
|
162
|
+
group and the SSM parameter that made the same trip last month. Around two
|
|
163
|
+
minutes end to end.
|
|
164
|
+
|
|
165
|
+
## Two tiers
|
|
166
|
+
|
|
167
|
+
**Offline is the default.** The synthetic `terraform.tfstate` is committed, so
|
|
168
|
+
`carve advise` and `carve emit --state` run with no Docker, no terraform binary,
|
|
169
|
+
no AWS account and no network. The only dependency beyond this directory is
|
|
170
|
+
`@cdktf/hcl2json`, which chant lazy-loads and names in its error if absent. This
|
|
171
|
+
tier guarantees the first thirty seconds of the video.
|
|
172
|
+
|
|
173
|
+
**`--live` is the full video.** Boot a scratch Floci, arm
|
|
174
|
+
`legacy-tf/floci-override.tf.disabled` (see its header), and `terraform apply`
|
|
175
|
+
the estate into it, so the state is one terraform really wrote. Then the observe
|
|
176
|
+
beats become footage rather than caption: after Emit, `chant lifecycle diff
|
|
177
|
+
--live` reads the bucket out of Floci, clean, while Terraform still owns it; the
|
|
178
|
+
handoff runs a real `terraform plan` showing no destroy; and behold's overlay
|
|
179
|
+
flips the bucket green afterwards. The line it exists for is "Terraform forgot
|
|
180
|
+
it, chant adopted it, and it never blinked."
|
|
181
|
+
|
|
182
|
+
The live tier needs `docker` and `terraform` on PATH, boots its own throwaway
|
|
183
|
+
Floci and deletes it after, and never touches an existing `floci*` container.
|
|
184
|
+
Floci's emulation bounds it: S3 and CloudWatch Logs, the two types the
|
|
185
|
+
walkthrough actually carves, are covered (chant's `just carve-emit-e2e` is the
|
|
186
|
+
prior art). The VPC, lambda and CloudFront blocks are scenery for the advisor's
|
|
187
|
+
grey band, so expect `-target` rather than one apply of the whole estate.
|
|
188
|
+
|
|
189
|
+
## Why build still fails
|
|
190
|
+
|
|
191
|
+
`carve emit --state --select aws_s3_bucket.assets` reports "Folded in:
|
|
192
|
+
`aws_s3_bucket_public_access_block.assets`, `aws_s3_bucket_versioning.assets`",
|
|
193
|
+
and as of chant 0.44.7 (chant#1637, chant PR #1640) the emitted `Bucket`
|
|
194
|
+
actually carries both: `VersioningConfiguration` and
|
|
195
|
+
`PublicAccessBlockConfiguration` come out as native props, not an "unmapped
|
|
196
|
+
Terraform attributes" comment. That used to be the rough edge here — a fold
|
|
197
|
+
that was reported but not applied, so `chant build` failed two AWS policy
|
|
198
|
+
rules on source the advisor called clean. It is fixed now.
|
|
199
|
+
|
|
200
|
+
`chant lint` on the emitted project passes with warnings. `chant build` still
|
|
201
|
+
fails, but on one rule: WAW042, no bucket policy denying non-TLS requests.
|
|
202
|
+
That is not a carve-tooling gap — grep `legacy-tf/` for
|
|
203
|
+
`aws_s3_bucket_policy` and there isn't one. The Terraform never declared a
|
|
204
|
+
TLS-deny policy for this bucket, in any form; chant's post-synth checks
|
|
205
|
+
noticed the moment the resource had a native representation to check. Nothing
|
|
206
|
+
here would show up scanning the `.tf` by eye, because there is nothing to
|
|
207
|
+
find — only an absence.
|
|
208
|
+
|
|
209
|
+
Verified against chant 0.44.7. The walkthrough's Emit beat still shows `lint`,
|
|
210
|
+
not `build` — the step's "why lint and not build?" note says why on screen —
|
|
211
|
+
but the reason changed: `build` doesn't fail because the fold is incomplete
|
|
212
|
+
anymore, it fails because the estate really does have a policy gap. "chant
|
|
213
|
+
found a policy gap Terraform never noticed" is the honest line to close the
|
|
214
|
+
video on.
|
|
215
|
+
|
|
216
|
+
## Values
|
|
217
|
+
|
|
218
|
+
Every name here is a boring-realistic stand-in for a small production estate,
|
|
219
|
+
because the video pauses on the inspect pane. There is no `foo` and no `bar`.
|
|
220
|
+
The account id is `000000000000` and every ARN, id and hostname is fake; the
|
|
221
|
+
state file says so in its `_fixture_note` output.
|