@suss/contract-terraform 0.31.2 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -4
- package/dist/index.cjs +876 -155
- package/dist/index.d.cts +199 -36
- package/dist/index.d.ts +199 -36
- package/dist/index.js +882 -156
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A contract reader. It walks `.tf` files and describes what it finds as boundarie
|
|
|
12
12
|
suss contract --from terraform infra/terraform/dynamodb -o tables.json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The path may be one file or the directory a module lives in, since a module states its resources across several files.
|
|
15
|
+
The path may be one file or the directory a module lives in, since a module states its resources across several files. A root module that only calls child modules is read too: see below.
|
|
16
16
|
|
|
17
17
|
## What it reads
|
|
18
18
|
|
|
@@ -42,11 +42,73 @@ Each secondary index becomes its own boundary, because a query through an index
|
|
|
42
42
|
|
|
43
43
|
A table declares its keys and lets every other attribute vary, so the contract says `fieldSet: "partial"` and the checker never calls an ordinary attribute unknown.
|
|
44
44
|
|
|
45
|
+
## A structure written as JSON
|
|
46
|
+
|
|
47
|
+
Several providers take a structure as a string rather than as blocks: a BigQuery table's schema, an ECS task's container definitions, an IAM policy document. Terraform gives an author two ways to write one, and both mean the same thing once deployed:
|
|
48
|
+
|
|
49
|
+
```hcl
|
|
50
|
+
schema = jsonencode([
|
|
51
|
+
{ name = "order_id", type = "STRING", mode = "REQUIRED" },
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
schema = <<EOF
|
|
55
|
+
[{ "name": "order_id", "type": "STRING", "mode": "REQUIRED" }]
|
|
56
|
+
EOF
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`jsonAttributeValue` reads both, and a pack asks for one through `fieldsFromJson` on a storage entry, saying which attribute the JSON is at and which keys of an entry give the field its name, its type, and whether it is always set. A schema written this way is every field the item has, so the contract comes out `exhaustive`. One a file or a variable supplies is not written down anywhere the reader can see, so nothing is recorded rather than guessed.
|
|
60
|
+
|
|
61
|
+
A deployable steps into such an attribute rather than reading fields out of it. An ECS task writes its containers as `container_definitions = jsonencode([...])`, and a pack puts that attribute where it would put a block, so each container is read the same way a Cloud Run container written as blocks is.
|
|
62
|
+
|
|
63
|
+
## A block written once and deployed many times
|
|
64
|
+
|
|
65
|
+
A module rarely writes a container's variables out one block at a time. It writes one `dynamic "env"` block over a map, or, in an ECS task's JSON, a `for` expression over the same thing. Both are expanded where the configuration already says what is in that map, so the container declares the variables the deployment will give it rather than none at all. [How a repeated block is expanded](./DESIGN.md#a-block-written-once-and-deployed-many-times) says which spellings settle and which stay holes.
|
|
66
|
+
|
|
67
|
+
## A root module that calls child modules
|
|
68
|
+
|
|
69
|
+
Plenty of configurations declare no resources at all at the root. They call a module per service, and every table and function is inside the child. A `source` that says which directory beside this one is followed and read as a module of its own, and everything the child declares gets `module.orders.` in front of its summary name and its deployable unit. [A module call this reader follows](./DESIGN.md#a-module-call-this-reader-follows) says what crosses between a call and the module it calls.
|
|
70
|
+
|
|
71
|
+
## Several entries for one resource type
|
|
72
|
+
|
|
73
|
+
A pack states more than one entry for a resource type when the provider spells it differently across versions. A resource that runs whichever engine one of its attributes picks needs no second entry: a storage entry's `storageSystem` is either a word or an attribute and a table of what each value there means, so `aws_db_instance` reads its `engine` and `google_sql_database_instance` reads its `database_version`. A table matches whole values by default, or the start of a value under `matches: "prefix"`, which is what Cloud SQL needs: `database_version` states an engine and a release together, `POSTGRES_15` and `MYSQL_8_0_31`, and the releases change every quarter.
|
|
74
|
+
|
|
75
|
+
A value the pack does not list, and a value the configuration builds at deploy time, both leave the store with no engine on it. The resource is still read: the instance is deployed and it has a name, and the summary records a gap saying which attribute went unsettled. `appliesWhen` is for the other case, a resource that is not the thing the entry describes at all. A Firestore database in Datastore mode speaks a different API, so that entry gates on `type` and skips the resource outright.
|
|
76
|
+
|
|
77
|
+
## What a deployable declares
|
|
78
|
+
|
|
79
|
+
A resource that deploys something becomes a runtime-config boundary, the same one `@suss/contract-cloudformation` writes for a Lambda in a template:
|
|
80
|
+
|
|
81
|
+
```hcl
|
|
82
|
+
resource "aws_lambda_function" "confirm" {
|
|
83
|
+
function_name = "${local.environment}-confirm"
|
|
84
|
+
handler = "src/handlers/confirm.handler"
|
|
85
|
+
|
|
86
|
+
environment {
|
|
87
|
+
variables = {
|
|
88
|
+
ORDERS_TABLE = aws_dynamodb_table.orders.name
|
|
89
|
+
LOG_LEVEL = "info"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The contract lists every variable the process starts with, the ones the configuration sets and the ones the platform adds, and says which is which. `LOG_LEVEL` is recorded as the text it is set to. `ORDERS_TABLE` is set to another resource, so the contract records both what that resource states its name to be and which resource it was, and code that reads `process.env.ORDERS_TABLE` to address a table reaches the table through either.
|
|
96
|
+
|
|
97
|
+
The unit is the resource label, `confirm`, rather than the name it deploys under. The label is what the rest of the configuration refers to it as, the way a logical id is in CloudFormation, so a variable pointing at the resource and the resource's own contract spell it the same way.
|
|
98
|
+
|
|
99
|
+
One resource deploys several processes where the provider says so. An ECS task definition is one contract per container, and each container gets only its own variables.
|
|
100
|
+
|
|
101
|
+
Every unsettled value in a contract is spelled the same way. An `image = var.image`, a `runtime` and a handler a variable supplies all come out as the pattern `{var.image}` rather than as the raw `${var.image}`, so nothing downstream has to know which field it is reading. A handler with a hole in it says nothing about which file the code is in, since splitting it at its last dot would pick a module nobody deploys.
|
|
102
|
+
|
|
103
|
+
A configuration says which handler runs and never which directory the deployed artifact was built from, so the handler is all the checker has to go on. A container deployable states no handler at all, since its image was built somewhere else, and then the caller says where the code is with `--code-scope api/web=services/api`, or `codeScopes` on the read options. [Which code a deployable unit runs](./DESIGN.md#which-code-a-deployable-unit-runs) says what that writes, and what happens when two units are pointed at one directory.
|
|
104
|
+
|
|
45
105
|
## What it will not tell you
|
|
46
106
|
|
|
47
107
|
- **Only what a loaded pack describes.** A resource no entry covers, an IAM policy or a subnet, is skipped.
|
|
48
|
-
- **A resource built with `for_each` or `count`** states one block for many tables, and this reads the block as written rather than working out what it expands to.
|
|
108
|
+
- **A resource built with `for_each` or `count`** states one `resource` block for many tables, and this reads the block as written rather than working out what it expands to. A `dynamic` block inside a resource is expanded, since that one decides what a single deployed process is given.
|
|
49
109
|
- **A name a variable supplies whole**, `name = var.table_name`, has no fixed text to pair on, so it records nothing rather than guessing.
|
|
110
|
+
- **What a secret contains.** A variable a secret supplies records which resource supplies it and nothing else, since no configuration writes the contents down.
|
|
111
|
+
- **Which source directory a deployable was built from.** Terraform builds the artifact outside the configuration, so a `filename` is a zip nothing in the run can open and a `source_dir` belongs to a data source this does not follow. `--code-scope` is how the caller supplies what the configuration never says.
|
|
50
112
|
|
|
51
113
|
## Where it fits in suss
|
|
52
114
|
|
|
@@ -54,6 +116,6 @@ Depends on `@suss/behavioral-ir` for the summaries it produces and `hcl2-parser`
|
|
|
54
116
|
|
|
55
117
|
## More
|
|
56
118
|
|
|
57
|
-
- [How it
|
|
58
|
-
- [Documentation](https://
|
|
119
|
+
- [How it reads a configuration](./DESIGN.md)
|
|
120
|
+
- [Documentation](https://suss.sh/)
|
|
59
121
|
- [Source and issues](https://github.com/nimbuscloud-ai/suss)
|