@suss/contract-terraform 0.9.0 → 0.11.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 CHANGED
@@ -83,7 +83,7 @@ An entry with `kind: "metric"` says which attribute is the name and how the depl
83
83
 
84
84
  The selector is a small query language rather than a name, so `parseFilterQuery` reads it: comparisons joined by `AND` and `OR`, with parentheses, `NOT`, quoted or bare values, and terms written next to each other for AND. A key may quote one of its own segments, `metric.label."response_code_class"`, and that comes back as one key with the quotes off. A call may stand where a comparison would, which is how Cloud Monitoring writes an SLO burn-rate condition, and `filterCalls` gives those back. The parser is exported, so a pack that has to read the same string for something else does not write a second one. A selector it cannot read leaves the reading with no metric on it, which pairs with nothing.
85
85
 
86
- Both kinds also say what the resource's own words mean in suss's terms, so the checker never learns a provider's vocabulary. A pack writes `values: { attribute: "shape.value_type", means: { SPREAD: "spread", SCALAR: "number" } }`, the reader takes the value at that attribute, dotted paths stepping into nested blocks, looks it up in the table, and writes the answer to `metricContract` on the summary. `accumulates` works the same way. A value the table does not list says nothing, the same as an attribute the configuration never set.
86
+ Both kinds also say what the resource's own words mean in suss's terms, so the checker never learns a provider's vocabulary. A pack writes `values: { attribute: "shape.value_type", means: { SPREAD: "histogram", SCALAR: "number" } }`, the reader takes the value at that attribute, dotted paths stepping into nested blocks, looks it up in the table, and writes the answer to `metricContract` on the summary. `accumulates` works the same way. A value the table does not list says nothing, the same as an attribute the configuration never set.
87
87
 
88
88
  A reading translates two things the same way. `comparesTo: { attribute: "limit", whenSet: "number" }` says the reading compares the series against a single number when it states a limit at all, since a threshold is a number by being written down. `reducesTo` maps an aligner or reducer to what it leaves behind. Both land on the summary under `metricReading`, along with the setting and the values that would reduce the series, so a finding can name the fix without the checker knowing which provider it came from.
89
89
 
package/dist/index.cjs CHANGED
@@ -394,7 +394,7 @@ function summariesForFiles(files, options) {
394
394
  for (const [resourceType, label, body] of file.resources) {
395
395
  for (const pack of options.packs) {
396
396
  const pattern = patternFor(pack, resourceType, file.constraints);
397
- if (pattern === void 0) {
397
+ if (pattern === void 0 || !entryApplies(pattern, body)) {
398
398
  continue;
399
399
  }
400
400
  summaries.push(
@@ -469,6 +469,18 @@ function patternFor(pack, resourceType, constraints) {
469
469
  }
470
470
  });
471
471
  }
472
+ function entryApplies(pattern, body) {
473
+ const gate = pattern.appliesWhen;
474
+ if (gate === void 0) {
475
+ return true;
476
+ }
477
+ const value = body[gate.attribute];
478
+ if (value === void 0) {
479
+ return gate.whenUnset === "read";
480
+ }
481
+ const text = stringOf(value);
482
+ return text !== null && gate.equals.includes(text);
483
+ }
472
484
  var READERS = {
473
485
  storage: (site, boundary) => {
474
486
  const types = fieldTypes(site.body, boundary);
@@ -488,7 +500,8 @@ function summariesFor(opts) {
488
500
  function storageSummary(opts) {
489
501
  const { boundary, label, shape, types } = opts;
490
502
  const address = `${opts.resourceType}.${label}`;
491
- const physicalTable = boundary.nameAttribute === void 0 ? null : namePattern(opts.body[boundary.nameAttribute], opts.scope);
503
+ const declaresContainer = boundary.declares !== "store";
504
+ const physicalTable = !declaresContainer || boundary.nameAttribute === void 0 ? null : namePattern(opts.body[boundary.nameAttribute], opts.scope);
492
505
  return {
493
506
  kind: "library",
494
507
  location: {
@@ -504,7 +517,7 @@ function storageSummary(opts) {
504
517
  storageSystem: boundary.storageSystem,
505
518
  ...boundary.transport !== void 0 ? { transport: boundary.transport } : {},
506
519
  scope: "default",
507
- container: label,
520
+ container: declaresContainer ? label : null,
508
521
  accessPath: shape.accessPath
509
522
  })
510
523
  },
package/dist/index.d.cts CHANGED
@@ -20,6 +20,17 @@ interface StorageResource {
20
20
  storageSystem: string;
21
21
  /** How a caller reaches it, when that is not the store's own name. */
22
22
  transport?: string;
23
+ /**
24
+ * What the resource declares. The default, `"container"`, means the
25
+ * resource is the thing code addresses: code passes a bucket's name
26
+ * to `bucket()`, so the declared name and the accessed name meet.
27
+ * `"store"` means the resource only says the store exists. Code
28
+ * splits a Redis cluster into key namespaces, no attribute of the
29
+ * cluster lists them, so the summary gets no container name and
30
+ * claims no access. Any match on the cluster's own name would be a
31
+ * coincidence between a deployment name and a key prefix.
32
+ */
33
+ declares?: "container" | "store";
23
34
  /** The attribute that says what the resource is called once deployed. */
24
35
  nameAttribute?: string;
25
36
  /** Whether the fields it declares are every field an item has. */
@@ -80,7 +91,7 @@ interface MetricResource {
80
91
  * metric spells, so it is the identity the two sides share.
81
92
  */
82
93
  metricTypeTemplate: string;
83
- /** Which attribute says whether one measurement is a number or a spread. */
94
+ /** Which attribute says whether one measurement is a number or a histogram. */
84
95
  values?: AttributeMeaning<MetricValueShape>;
85
96
  /** Which attribute says what a measurement covers. */
86
97
  accumulates?: AttributeMeaning<MetricAccumulation>;
@@ -116,6 +127,20 @@ type TerraformResource = StorageResource | MessageBusResource | MetricResource |
116
127
  interface TerraformResourcePattern {
117
128
  /** The resource type, spelled the way the provider spells it. */
118
129
  resource: string;
130
+ /**
131
+ * An attribute that decides whether the entry describes the resource
132
+ * at all. `aws_elasticache_cluster` deploys whichever engine its
133
+ * `engine` attribute picks, and only some engines are the store the
134
+ * entry describes. A value outside `equals`, or one built at deploy
135
+ * time, means the resource is not read, rather than read as
136
+ * something it may not be. `whenUnset` says what an absent attribute
137
+ * means; the default is not to read the resource.
138
+ */
139
+ appliesWhen?: {
140
+ attribute: string;
141
+ equals: string[];
142
+ whenUnset?: "read" | "skip";
143
+ };
119
144
  /**
120
145
  * Which provider versions this describes, as a semver range. A
121
146
  * configuration states its own constraint under `required_providers`,
package/dist/index.d.ts CHANGED
@@ -20,6 +20,17 @@ interface StorageResource {
20
20
  storageSystem: string;
21
21
  /** How a caller reaches it, when that is not the store's own name. */
22
22
  transport?: string;
23
+ /**
24
+ * What the resource declares. The default, `"container"`, means the
25
+ * resource is the thing code addresses: code passes a bucket's name
26
+ * to `bucket()`, so the declared name and the accessed name meet.
27
+ * `"store"` means the resource only says the store exists. Code
28
+ * splits a Redis cluster into key namespaces, no attribute of the
29
+ * cluster lists them, so the summary gets no container name and
30
+ * claims no access. Any match on the cluster's own name would be a
31
+ * coincidence between a deployment name and a key prefix.
32
+ */
33
+ declares?: "container" | "store";
23
34
  /** The attribute that says what the resource is called once deployed. */
24
35
  nameAttribute?: string;
25
36
  /** Whether the fields it declares are every field an item has. */
@@ -80,7 +91,7 @@ interface MetricResource {
80
91
  * metric spells, so it is the identity the two sides share.
81
92
  */
82
93
  metricTypeTemplate: string;
83
- /** Which attribute says whether one measurement is a number or a spread. */
94
+ /** Which attribute says whether one measurement is a number or a histogram. */
84
95
  values?: AttributeMeaning<MetricValueShape>;
85
96
  /** Which attribute says what a measurement covers. */
86
97
  accumulates?: AttributeMeaning<MetricAccumulation>;
@@ -116,6 +127,20 @@ type TerraformResource = StorageResource | MessageBusResource | MetricResource |
116
127
  interface TerraformResourcePattern {
117
128
  /** The resource type, spelled the way the provider spells it. */
118
129
  resource: string;
130
+ /**
131
+ * An attribute that decides whether the entry describes the resource
132
+ * at all. `aws_elasticache_cluster` deploys whichever engine its
133
+ * `engine` attribute picks, and only some engines are the store the
134
+ * entry describes. A value outside `equals`, or one built at deploy
135
+ * time, means the resource is not read, rather than read as
136
+ * something it may not be. `whenUnset` says what an absent attribute
137
+ * means; the default is not to read the resource.
138
+ */
139
+ appliesWhen?: {
140
+ attribute: string;
141
+ equals: string[];
142
+ whenUnset?: "read" | "skip";
143
+ };
119
144
  /**
120
145
  * Which provider versions this describes, as a semver range. A
121
146
  * configuration states its own constraint under `required_providers`,
package/dist/index.js CHANGED
@@ -360,7 +360,7 @@ function summariesForFiles(files, options) {
360
360
  for (const [resourceType, label, body] of file.resources) {
361
361
  for (const pack of options.packs) {
362
362
  const pattern = patternFor(pack, resourceType, file.constraints);
363
- if (pattern === void 0) {
363
+ if (pattern === void 0 || !entryApplies(pattern, body)) {
364
364
  continue;
365
365
  }
366
366
  summaries.push(
@@ -435,6 +435,18 @@ function patternFor(pack, resourceType, constraints) {
435
435
  }
436
436
  });
437
437
  }
438
+ function entryApplies(pattern, body) {
439
+ const gate = pattern.appliesWhen;
440
+ if (gate === void 0) {
441
+ return true;
442
+ }
443
+ const value = body[gate.attribute];
444
+ if (value === void 0) {
445
+ return gate.whenUnset === "read";
446
+ }
447
+ const text = stringOf(value);
448
+ return text !== null && gate.equals.includes(text);
449
+ }
438
450
  var READERS = {
439
451
  storage: (site, boundary) => {
440
452
  const types = fieldTypes(site.body, boundary);
@@ -454,7 +466,8 @@ function summariesFor(opts) {
454
466
  function storageSummary(opts) {
455
467
  const { boundary, label, shape, types } = opts;
456
468
  const address = `${opts.resourceType}.${label}`;
457
- const physicalTable = boundary.nameAttribute === void 0 ? null : namePattern(opts.body[boundary.nameAttribute], opts.scope);
469
+ const declaresContainer = boundary.declares !== "store";
470
+ const physicalTable = !declaresContainer || boundary.nameAttribute === void 0 ? null : namePattern(opts.body[boundary.nameAttribute], opts.scope);
458
471
  return {
459
472
  kind: "library",
460
473
  location: {
@@ -470,7 +483,7 @@ function storageSummary(opts) {
470
483
  storageSystem: boundary.storageSystem,
471
484
  ...boundary.transport !== void 0 ? { transport: boundary.transport } : {},
472
485
  scope: "default",
473
- container: label,
486
+ container: declaresContainer ? label : null,
474
487
  accessPath: shape.accessPath
475
488
  })
476
489
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/contract-terraform",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Read the storage a Terraform configuration declares, for checking code against it.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@suss/behavioral-ir": "0.9.0",
48
+ "@suss/behavioral-ir": "0.11.0",
49
49
  "hcl2-parser": "^1.0.3",
50
50
  "semver": "^7.6.0"
51
51
  },