@intentius/chant 0.44.14 → 0.46.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/dist/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +21 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +85 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +275 -5
- package/src/cli/commands/build.ts +86 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +46 -2
- package/src/cli/commands/check-lexicon.ts +64 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +10 -1
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +24 -4
- package/src/cli/handlers/lifecycle.test.ts +348 -1
- package/src/cli/handlers/lifecycle.ts +235 -3
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +60 -8
- package/src/cli/main.ts +17 -4
- package/src/cli/registry.ts +7 -0
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +22 -2
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +49 -3
- package/src/codegen/validate.ts +36 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +159 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/policy.ts +14 -3
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/index.ts +4 -1
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintRule, LintContext, LintDiagnostic, LintProjectConfig } from "../rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* COR021: Literal Name in a Multi-Environment Project (#1221)
|
|
6
|
+
*
|
|
7
|
+
* A project that declares two or more `environments` and binds its ownership
|
|
8
|
+
* marker to a build parameter (`ownership.env: { param: "env" }`) is built
|
|
9
|
+
* once per environment — and every physical name that does not vary with
|
|
10
|
+
* that parameter is the SAME name in every build. The collision is silent at
|
|
11
|
+
* build time and only surfaces at apply time, when the second environment's
|
|
12
|
+
* deploy walks over the first's resources.
|
|
13
|
+
*
|
|
14
|
+
* This rule warns on the declaration: a name-bearing property (`name`, or a
|
|
15
|
+
* `*Name` property like `bucketName`) whose value is a bare string literal,
|
|
16
|
+
* in a project shaped for per-environment builds. The fix is interpolation —
|
|
17
|
+
* `` `billing-${params.env}-uploads` `` — which folds to a per-environment
|
|
18
|
+
* literal because build parameters resolve before any file is imported
|
|
19
|
+
* (#1064). See the resource-naming guide's multi-environment section.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately silent when:
|
|
22
|
+
* - no project config was threaded (a bare unit test, the LSP single-file path),
|
|
23
|
+
* - the project declares fewer than two environments (nothing to collide),
|
|
24
|
+
* - `ownership.env` is not param-bound — a literal `ownership.env` (or none)
|
|
25
|
+
* means the project is not doing per-environment builds from one source
|
|
26
|
+
* tree, so per-instance names are presumably managed another way (the
|
|
27
|
+
* layered-config all-in-one pattern hand-names each instance),
|
|
28
|
+
* - the value is anything other than a bare string literal — a template
|
|
29
|
+
* interpolating `params.<name>` is the fixed shape, and other non-literal
|
|
30
|
+
* values are EVL territory, not this rule's.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Does this property name carry a physical resource name — `name` or a camelCase `*Name`? */
|
|
34
|
+
function isNameBearingProperty(propName: string): boolean {
|
|
35
|
+
return propName === "name" || /^[a-z][A-Za-z0-9]*Name$/.test(propName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The property's declared name, for Identifier and string-literal keys; undefined for computed keys. */
|
|
39
|
+
function propertyName(prop: ts.PropertyAssignment): string | undefined {
|
|
40
|
+
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) return prop.name.text;
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether the rule's preconditions hold: 2+ declared environments and a param-bound ownership.env. */
|
|
45
|
+
function projectIsMultiEnvParamBound(config: LintProjectConfig | undefined): string | undefined {
|
|
46
|
+
const env = config?.ownership?.env;
|
|
47
|
+
const paramBound = typeof env === "object" && env !== null && typeof env.param === "string";
|
|
48
|
+
if (!paramBound) return undefined;
|
|
49
|
+
if ((config?.environments?.length ?? 0) < 2) return undefined;
|
|
50
|
+
return env.param;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Walk an object literal (nested included, e.g. k8s `metadata: { name }`), flagging literal name-bearing values. */
|
|
54
|
+
function checkObjectLiteral(
|
|
55
|
+
obj: ts.ObjectLiteralExpression,
|
|
56
|
+
paramName: string,
|
|
57
|
+
context: LintContext,
|
|
58
|
+
diagnostics: LintDiagnostic[],
|
|
59
|
+
): void {
|
|
60
|
+
for (const prop of obj.properties) {
|
|
61
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
62
|
+
const name = propertyName(prop);
|
|
63
|
+
const value = prop.initializer;
|
|
64
|
+
if (
|
|
65
|
+
name !== undefined &&
|
|
66
|
+
isNameBearingProperty(name) &&
|
|
67
|
+
(ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
68
|
+
) {
|
|
69
|
+
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
70
|
+
value.getStart(context.sourceFile),
|
|
71
|
+
);
|
|
72
|
+
diagnostics.push({
|
|
73
|
+
file: context.filePath,
|
|
74
|
+
line: line + 1,
|
|
75
|
+
column: character + 1,
|
|
76
|
+
ruleId: "COR021",
|
|
77
|
+
severity: "warning",
|
|
78
|
+
message:
|
|
79
|
+
`Literal ${name}: "${value.text}" in a multi-environment project — every environment's build ` +
|
|
80
|
+
`produces this same physical name, so two deployed environments collide. Interpolate the env ` +
|
|
81
|
+
`parameter: \`${value.text}-\${params.${paramName}}\` (see the resource-naming guide's ` +
|
|
82
|
+
`multi-environment section).`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
86
|
+
checkObjectLiteral(value, paramName, context, diagnostics);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkNode(node: ts.Node, paramName: string, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
92
|
+
if (ts.isNewExpression(node) && node.arguments && node.arguments.length > 0) {
|
|
93
|
+
const firstArg = node.arguments[0];
|
|
94
|
+
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
95
|
+
checkObjectLiteral(firstArg, paramName, context, diagnostics);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
ts.forEachChild(node, (child) => checkNode(child, paramName, context, diagnostics));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const cor021EnvLiteralNameRule: LintRule = {
|
|
102
|
+
id: "COR021",
|
|
103
|
+
severity: "warning",
|
|
104
|
+
category: "correctness",
|
|
105
|
+
description:
|
|
106
|
+
"In a multi-environment project with a param-bound ownership.env, name-bearing properties should interpolate the env parameter, not hold a bare literal",
|
|
107
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
108
|
+
const paramName = projectIsMultiEnvParamBound(context.projectConfig);
|
|
109
|
+
if (paramName === undefined) return [];
|
|
110
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
111
|
+
checkNode(context.sourceFile, paramName, context, diagnostics);
|
|
112
|
+
return diagnostics;
|
|
113
|
+
},
|
|
114
|
+
};
|
package/src/lint/rules/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
25
25
|
export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
26
26
|
export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
27
27
|
export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
28
|
+
export { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
28
29
|
export { isInsideCompositeFactory } from "./composite-scope";
|
|
29
30
|
|
|
30
31
|
import { flatDeclarationsRule } from "./flat-declarations";
|
|
@@ -48,9 +49,10 @@ import { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
48
49
|
import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
49
50
|
import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
50
51
|
import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
52
|
+
import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
* Load all
|
|
55
|
+
* Load all 22 core lint rules (COR + EVL).
|
|
54
56
|
*/
|
|
55
57
|
export function loadCoreRules(): LintRule[] {
|
|
56
58
|
return [
|
|
@@ -75,5 +77,6 @@ export function loadCoreRules(): LintRule[] {
|
|
|
75
77
|
evl010CompositeNoTransformRule,
|
|
76
78
|
cor017CompositeNameMatchRule,
|
|
77
79
|
cor018CompositePreferLexiconTypeRule,
|
|
80
|
+
cor021EnvLiteralNameRule,
|
|
78
81
|
];
|
|
79
82
|
}
|
|
@@ -2,28 +2,16 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS,
|
|
4
4
|
k8sListMapOrderKey,
|
|
5
|
+
K8S_SYSTEM_METADATA_PRUNE_PATTERNS,
|
|
5
6
|
buildOwnershipSets,
|
|
6
|
-
pruneByOwnership,
|
|
7
|
-
type OwnershipSets,
|
|
8
7
|
} from "./managed-fields";
|
|
9
|
-
import { normalizeDeepProperties
|
|
8
|
+
import { normalizeDeepProperties } from "./deep-observation";
|
|
10
9
|
|
|
11
10
|
/** The naming scheme both the k8s lexicon (via `@intentius/chant-k8s-client`'s `isChantFieldManager`) and gcp restate use in their own tests. */
|
|
12
11
|
function isChantManager(manager: string | undefined): boolean {
|
|
13
12
|
return !!manager && (manager === "chant" || manager.startsWith("chant:"));
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
function node(partial: Partial<DeepNode> & Pick<DeepNode, "path" | "pattern">): DeepNode {
|
|
17
|
-
return {
|
|
18
|
-
entityType: "Test::Entity",
|
|
19
|
-
key: partial.pattern,
|
|
20
|
-
value: undefined,
|
|
21
|
-
side: "live",
|
|
22
|
-
counterpart: "unknown",
|
|
23
|
-
...partial,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
15
|
describe("K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS — the generic Kubernetes object envelope", () => {
|
|
28
16
|
test("covers status and the server-minted metadata fields", () => {
|
|
29
17
|
for (const p of [
|
|
@@ -150,36 +138,21 @@ describe("buildOwnershipSets — resolving managedFields against live and declar
|
|
|
150
138
|
});
|
|
151
139
|
});
|
|
152
140
|
|
|
153
|
-
describe("
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
test("never prunes the declared side", () => {
|
|
166
|
-
expect(pruneByOwnership(node({ path: "spec.replicas", pattern: "spec.replicas", side: "declared" }), sets)).toBe(false);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("never prunes a chant-owned path", () => {
|
|
170
|
-
expect(pruneByOwnership(node({ path: "metadata.labels.tier", pattern: "metadata.labels.tier" }), sets)).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("prunes a foreign-owned, uncontested (undeclared) path", () => {
|
|
174
|
-
expect(pruneByOwnership(node({ path: "metadata.annotations.noise", pattern: "metadata.annotations.noise" }), sets)).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("keeps a foreign-owned, contested (declared) path", () => {
|
|
178
|
-
expect(pruneByOwnership(node({ path: "spec.replicas", pattern: "spec.replicas" }), sets)).toBe(false);
|
|
141
|
+
describe("K8S_SYSTEM_METADATA_PRUNE_PATTERNS — what the controllers stamp, not what a human labels (#1191)", () => {
|
|
142
|
+
test("covers client-side apply bookkeeping, the Deployment controller's rollout counters and the controllers' selector labels", () => {
|
|
143
|
+
for (const p of [
|
|
144
|
+
"metadata.annotations.kubectl.kubernetes.io/last-applied-configuration",
|
|
145
|
+
"metadata.annotations.deployment.kubernetes.io/revision",
|
|
146
|
+
"metadata.labels.pod-template-hash",
|
|
147
|
+
"metadata.labels.controller-revision-hash",
|
|
148
|
+
]) {
|
|
149
|
+
expect(K8S_SYSTEM_METADATA_PRUNE_PATTERNS.has(p)).toBe(true);
|
|
150
|
+
}
|
|
179
151
|
});
|
|
180
152
|
|
|
181
|
-
test("
|
|
182
|
-
expect(
|
|
153
|
+
test("an out-of-band user label is not on the list — it has to reach the diff as undeclared", () => {
|
|
154
|
+
expect(K8S_SYSTEM_METADATA_PRUNE_PATTERNS.has("metadata.labels.team")).toBe(false);
|
|
155
|
+
expect(K8S_SYSTEM_METADATA_PRUNE_PATTERNS.has("metadata.annotations.kubernetes.io/change-cause")).toBe(false);
|
|
183
156
|
});
|
|
184
157
|
});
|
|
185
158
|
|
package/src/managed-fields.ts
CHANGED
|
@@ -142,17 +142,18 @@ export interface OwnershipSets {
|
|
|
142
142
|
chantOwned: ReadonlySet<string>;
|
|
143
143
|
/** Paths owned by a manager that is not chant. */
|
|
144
144
|
foreignOwned: ReadonlySet<string>;
|
|
145
|
-
/** The subset of `foreignOwned` where the declared manifest also sets the path —
|
|
145
|
+
/** The subset of `foreignOwned` where the declared manifest also sets the path — a contested field. */
|
|
146
146
|
foreignContested: ReadonlySet<string>;
|
|
147
147
|
/**
|
|
148
148
|
* Path → the name of the manager that owns it (#1189).
|
|
149
149
|
*
|
|
150
|
-
* The three sets above answer *which category* owns a path
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
150
|
+
* The three sets above answer *which category* owns a path. A reader needs
|
|
151
|
+
* the other question — "owned by `kubectl-client-side-apply`" and "owned by
|
|
152
|
+
* `hpa-controller`" are the same category and mean very different things to
|
|
153
|
+
* an operator. Since chant #1191 no category is pruned from the diff, so
|
|
154
|
+
* this map is what tells the two apart in the report. Last writer wins
|
|
155
|
+
* where several managers touch one path, matching what the API server
|
|
156
|
+
* itself reports.
|
|
156
157
|
*/
|
|
157
158
|
owners: ReadonlyMap<string, string>;
|
|
158
159
|
}
|
|
@@ -323,25 +324,41 @@ export function buildOwnershipSets(
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
327
|
+
* Well-known metadata the control plane writes on an object regardless of
|
|
328
|
+
* who applied it — matched on the exact index-erased pattern, like
|
|
329
|
+
* {@link K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS}, but gated by the caller on
|
|
330
|
+
* `side === "live" && counterpart === "absent"` so a manifest that *does*
|
|
331
|
+
* declare one of these (a hand-pinned `change-cause`, say) is still compared.
|
|
330
332
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
333
|
+
* This list is the noise valve that replaced the managed-fields prune
|
|
334
|
+
* (chant #1191). Until then a foreign-owned, undeclared path was dropped
|
|
335
|
+
* before the diff ever saw it, which silenced `kubectl label deploy web
|
|
336
|
+
* team=platform` — the exact console-edit class the `undeclared` drift kind
|
|
337
|
+
* exists for. Now a foreign-owned path nobody declared is reported as
|
|
338
|
+
* `undeclared` (with its owner named, #1189) unless the accepted baseline
|
|
339
|
+
* already carries it — and what remains to subtract statically is only the
|
|
340
|
+
* handful of annotations and labels Kubernetes' own controllers stamp on
|
|
341
|
+
* every object of a kind. Widening this set is additive; each entry must be
|
|
342
|
+
* something *no* human writes out of band.
|
|
337
343
|
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
344
|
+
* - `kubectl.kubernetes.io/last-applied-configuration` — client-side apply's
|
|
345
|
+
* bookkeeping, a JSON copy of whatever was last applied.
|
|
346
|
+
* - `deployment.kubernetes.io/revision` (+ `revision-history`,
|
|
347
|
+
* `desired-replicas`, `max-replicas`) — the Deployment controller's
|
|
348
|
+
* rollout counters, written to Deployments and their ReplicaSets.
|
|
349
|
+
* - `pod-template-hash` / `controller-revision-hash` /
|
|
350
|
+
* `statefulset.kubernetes.io/pod-name` — the selector labels the
|
|
351
|
+
* ReplicaSet, DaemonSet and StatefulSet controllers add to what they own.
|
|
340
352
|
*/
|
|
341
|
-
export
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
353
|
+
export const K8S_SYSTEM_METADATA_PRUNE_PATTERNS: ReadonlySet<string> = new Set([
|
|
354
|
+
"metadata.annotations.kubectl.kubernetes.io/last-applied-configuration",
|
|
355
|
+
"metadata.annotations.deployment.kubernetes.io/revision",
|
|
356
|
+
"metadata.annotations.deployment.kubernetes.io/revision-history",
|
|
357
|
+
"metadata.annotations.deployment.kubernetes.io/desired-replicas",
|
|
358
|
+
"metadata.annotations.deployment.kubernetes.io/max-replicas",
|
|
359
|
+
"metadata.labels.pod-template-hash",
|
|
360
|
+
"metadata.labels.controller-revision-hash",
|
|
361
|
+
"metadata.labels.statefulset.kubernetes.io/pod-name",
|
|
362
|
+
"spec.template.metadata.labels.pod-template-hash",
|
|
363
|
+
"spec.template.metadata.labels.controller-revision-hash",
|
|
364
|
+
]);
|
package/src/observation.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ const meta = (over: Partial<ResourceMetadata> = {}): ResourceMetadata => ({
|
|
|
29
29
|
|
|
30
30
|
describe("normalizeObservation", () => {
|
|
31
31
|
test("a bare map means 'I looked at everything'", () => {
|
|
32
|
-
expect(normalizeObservation({ a: meta() })).toEqual({ resources: { a: meta() }, unobserved: {}, queried: {} });
|
|
32
|
+
expect(normalizeObservation({ a: meta() })).toEqual({ resources: { a: meta() }, unobserved: {}, queried: {}, notes: [] });
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
test("the envelope carries both halves", () => {
|
|
@@ -38,11 +38,19 @@ describe("normalizeObservation", () => {
|
|
|
38
38
|
resources: { a: meta() },
|
|
39
39
|
unobserved: { b: { reason: "read-failed" } },
|
|
40
40
|
queried: {},
|
|
41
|
+
notes: [],
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
test("run-level notes ride the envelope, distinct (#1265)", () => {
|
|
46
|
+
const value = observation({ a: meta() }, undefined, undefined, ["ownership unknown", "ownership unknown"]);
|
|
47
|
+
expect(value.notes).toEqual(["ownership unknown", "ownership unknown"]);
|
|
48
|
+
expect(normalizeObservation(value).notes).toEqual(["ownership unknown"]);
|
|
49
|
+
expect(observation({}).notes).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
44
52
|
test("undefined normalizes to empty maps", () => {
|
|
45
|
-
expect(normalizeObservation(undefined)).toEqual({ resources: {}, unobserved: {}, queried: {} });
|
|
53
|
+
expect(normalizeObservation(undefined)).toEqual({ resources: {}, unobserved: {}, queried: {}, notes: [] });
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
test("the envelope carries the queried addresses through normalization (#1620)", () => {
|
|
@@ -84,25 +92,33 @@ describe("unobservedAll", () => {
|
|
|
84
92
|
describe("mergeObservations (multi-stack)", () => {
|
|
85
93
|
test("present beats not-observed beats absent", () => {
|
|
86
94
|
const merged = mergeObservations([
|
|
87
|
-
{ resources: {}, unobserved: { a: { reason: "read-failed" }, b: { reason: "no-binding" } }, queried: {} },
|
|
88
|
-
{ resources: { a: meta() }, unobserved: {}, queried: {} },
|
|
95
|
+
{ resources: {}, unobserved: { a: { reason: "read-failed" }, b: { reason: "no-binding" } }, queried: {}, notes: [] },
|
|
96
|
+
{ resources: { a: meta() }, unobserved: {}, queried: {}, notes: [] },
|
|
89
97
|
]);
|
|
90
98
|
expect(Object.keys(merged.resources)).toEqual(["a"]);
|
|
91
99
|
expect(Object.keys(merged.unobserved)).toEqual(["b"]);
|
|
92
100
|
});
|
|
93
101
|
|
|
102
|
+
test("the same note from four stacks is one note (#1265)", () => {
|
|
103
|
+
const note = "ownership filter unavailable";
|
|
104
|
+
const merged = mergeObservations(
|
|
105
|
+
["a", "b", "c", "d"].map((k) => ({ resources: { [k]: meta() }, unobserved: {}, queried: {}, notes: [note] })),
|
|
106
|
+
);
|
|
107
|
+
expect(merged.notes).toEqual([note]);
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
test("an entity nobody looked for in any stack stays absent", () => {
|
|
95
111
|
const merged = mergeObservations([
|
|
96
|
-
{ resources: { a: meta() }, unobserved: {}, queried: {} },
|
|
97
|
-
{ resources: { b: meta() }, unobserved: {}, queried: {} },
|
|
112
|
+
{ resources: { a: meta() }, unobserved: {}, queried: {}, notes: [] },
|
|
113
|
+
{ resources: { b: meta() }, unobserved: {}, queried: {}, notes: [] },
|
|
98
114
|
]);
|
|
99
115
|
expect(merged.unobserved).toEqual({});
|
|
100
116
|
});
|
|
101
117
|
|
|
102
118
|
test("queried addresses union across stacks (#1620)", () => {
|
|
103
119
|
const merged = mergeObservations([
|
|
104
|
-
{ resources: {}, unobserved: {}, queried: { a: "stack-1/a" } },
|
|
105
|
-
{ resources: { b: meta() }, unobserved: {}, queried: { b: "stack-2/b" } },
|
|
120
|
+
{ resources: {}, unobserved: {}, queried: { a: "stack-1/a" }, notes: [] },
|
|
121
|
+
{ resources: { b: meta() }, unobserved: {}, queried: { b: "stack-2/b" }, notes: [] },
|
|
106
122
|
]);
|
|
107
123
|
expect(merged.queried).toEqual({ a: "stack-1/a", b: "stack-2/b" });
|
|
108
124
|
});
|
package/src/observation.ts
CHANGED
|
@@ -112,6 +112,24 @@ export interface ObservationResult {
|
|
|
112
112
|
* namespace, endpoint or region was read, not the one the resource lives in.
|
|
113
113
|
*/
|
|
114
114
|
queried?: Record<string, string>;
|
|
115
|
+
/**
|
|
116
|
+
* Notices about the read as a whole, not about any one entity (#1265) —
|
|
117
|
+
* "the ownership filter could not be applied on this surface" is the
|
|
118
|
+
* canonical one. A note is a property of the environment or the read path,
|
|
119
|
+
* so core says each distinct note once per run, after the answer, however
|
|
120
|
+
* many stacks or lexicons reported it. A lexicon returns it here instead of
|
|
121
|
+
* printing, so the note cannot land ahead of the rows it qualifies.
|
|
122
|
+
*/
|
|
123
|
+
notes?: string[];
|
|
124
|
+
/**
|
|
125
|
+
* What the deployable unit publishes, keyed by the stack that publishes it
|
|
126
|
+
* (#1279) — a CloudFormation stack's outputs, for instance. Stack-level, so it
|
|
127
|
+
* lives here once rather than on every resource: a node's `attributes` are
|
|
128
|
+
* that resource's own properties, and a VPC that carried the stack's
|
|
129
|
+
* `expWebIp` beside no `CidrBlock` of its own was answering the wrong
|
|
130
|
+
* question. Values are already scrubbed of anything that looks secret.
|
|
131
|
+
*/
|
|
132
|
+
stackExports?: Record<string, Record<string, unknown>>;
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
/**
|
|
@@ -126,6 +144,10 @@ export interface NormalizedObservation {
|
|
|
126
144
|
unobserved: Record<string, UnobservedEntity>;
|
|
127
145
|
/** Resolved query address per entity name (#1620). Empty when the lexicon reported none. */
|
|
128
146
|
queried: Record<string, string>;
|
|
147
|
+
/** Run-level notices (#1265), distinct. Empty when the lexicon reported none. */
|
|
148
|
+
notes: string[];
|
|
149
|
+
/** Per-stack exports (#1279), keyed by stack name. Absent when the lexicon reported none. */
|
|
150
|
+
stackExports?: Record<string, Record<string, unknown>>;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
153
|
/** True when `value` is the versioned {@link ObservationResult} envelope. */
|
|
@@ -145,12 +167,16 @@ export function observation(
|
|
|
145
167
|
resources: Record<string, ResourceMetadata>,
|
|
146
168
|
unobserved?: Record<string, UnobservedEntity>,
|
|
147
169
|
queried?: Record<string, string>,
|
|
170
|
+
notes?: string[],
|
|
171
|
+
stackExports?: Record<string, Record<string, unknown>>,
|
|
148
172
|
): ObservationResult {
|
|
149
173
|
return {
|
|
150
174
|
observation: "v1",
|
|
151
175
|
resources,
|
|
152
176
|
...(unobserved && Object.keys(unobserved).length > 0 ? { unobserved } : {}),
|
|
153
177
|
...(queried && Object.keys(queried).length > 0 ? { queried } : {}),
|
|
178
|
+
...(notes && notes.length > 0 ? { notes } : {}),
|
|
179
|
+
...(stackExports && Object.keys(stackExports).length > 0 ? { stackExports } : {}),
|
|
154
180
|
};
|
|
155
181
|
}
|
|
156
182
|
|
|
@@ -161,11 +187,17 @@ export function observation(
|
|
|
161
187
|
* {@link unobservedAll} rather than returning nothing.
|
|
162
188
|
*/
|
|
163
189
|
export function normalizeObservation(value: DescribeResourcesResult | undefined): NormalizedObservation {
|
|
164
|
-
if (!value) return { resources: {}, unobserved: {}, queried: {} };
|
|
190
|
+
if (!value) return { resources: {}, unobserved: {}, queried: {}, notes: [] };
|
|
165
191
|
if (isObservationResult(value)) {
|
|
166
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
resources: value.resources ?? {},
|
|
194
|
+
unobserved: value.unobserved ?? {},
|
|
195
|
+
queried: value.queried ?? {},
|
|
196
|
+
notes: [...new Set(value.notes ?? [])],
|
|
197
|
+
...(value.stackExports && Object.keys(value.stackExports).length > 0 ? { stackExports: value.stackExports } : {}),
|
|
198
|
+
};
|
|
167
199
|
}
|
|
168
|
-
return { resources: value, unobserved: {}, queried: {} };
|
|
200
|
+
return { resources: value, unobserved: {}, queried: {}, notes: [] };
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
/**
|
|
@@ -205,15 +237,27 @@ export function mergeObservations(parts: Iterable<NormalizedObservation>): Norma
|
|
|
205
237
|
const resources: Record<string, ResourceMetadata> = {};
|
|
206
238
|
const unobserved: Record<string, UnobservedEntity> = {};
|
|
207
239
|
const queried: Record<string, string> = {};
|
|
240
|
+
// A note is about the read, not a stack; four stacks saying the same thing
|
|
241
|
+
// is one note (#1265).
|
|
242
|
+
const notes = new Set<string>();
|
|
243
|
+
const stackExports: Record<string, Record<string, unknown>> = {};
|
|
208
244
|
for (const part of parts) {
|
|
209
245
|
Object.assign(resources, part.resources);
|
|
210
246
|
Object.assign(unobserved, part.unobserved);
|
|
211
247
|
Object.assign(queried, part.queried);
|
|
248
|
+
for (const n of part.notes) notes.add(n);
|
|
249
|
+
Object.assign(stackExports, part.stackExports ?? {});
|
|
212
250
|
}
|
|
213
251
|
// Present wins: a stack that could not be read does not un-observe a resource
|
|
214
252
|
// another stack returned.
|
|
215
253
|
for (const name of Object.keys(resources)) delete unobserved[name];
|
|
216
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
resources,
|
|
256
|
+
unobserved,
|
|
257
|
+
queried,
|
|
258
|
+
notes: [...notes],
|
|
259
|
+
...(Object.keys(stackExports).length > 0 ? { stackExports } : {}),
|
|
260
|
+
};
|
|
217
261
|
}
|
|
218
262
|
|
|
219
263
|
/** One-line human phrasing of a reason, for CLI output. */
|
package/src/op/builders.ts
CHANGED
|
@@ -137,6 +137,37 @@ export const shell = (
|
|
|
137
137
|
export const teardown = (path: string): ActivityStep =>
|
|
138
138
|
activity("chantTeardown", { path }, "longInfra");
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Tear down one environment's marker-owned resources — the durable form of
|
|
142
|
+
* `chant lifecycle teardown <env> --yes` (#1222). The activity runs core's
|
|
143
|
+
* teardown engine in-process: enumerate by ownership marker (this project's
|
|
144
|
+
* `ownership.stack` + `env`), delete through each lexicon's `executeTeardown`
|
|
145
|
+
* capability, retry failures once, and fail the step when any candidate is
|
|
146
|
+
* still failed. Distinct from {@link teardown}, which runs a project's own
|
|
147
|
+
* `npm run teardown` script.
|
|
148
|
+
*
|
|
149
|
+
* The CLI's guards apply unchanged, and a production-like environment name
|
|
150
|
+
* needs `confirmProd: true` in `opts` — the authored counterpart of
|
|
151
|
+
* `--confirm-prod`, since an Op never prompts. An ordinary {@link gate} step
|
|
152
|
+
* placed before this one composes as usual (steps run in authored order), so
|
|
153
|
+
* a human approval can precede the deletion:
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* phase("Teardown", [
|
|
157
|
+
* gate("approve-teardown", { description: "Release the staging teardown" }),
|
|
158
|
+
* envTeardown("staging"),
|
|
159
|
+
* ]),
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* `opts` also accepts `path` (the chant project directory, default the
|
|
163
|
+
* worker's cwd). Defaults to the `longInfra` profile (override via
|
|
164
|
+
* `opts.profile`).
|
|
165
|
+
*/
|
|
166
|
+
export const envTeardown = (env: string, opts?: Record<string, unknown>): ActivityStep => {
|
|
167
|
+
const { args, profile } = takeProfile(opts);
|
|
168
|
+
return activity("envTeardown", { env, ...args }, profile ?? "longInfra");
|
|
169
|
+
};
|
|
170
|
+
|
|
140
171
|
/**
|
|
141
172
|
* Create a local k3d cluster (vanilla Kubernetes in Docker). Idempotent: skips
|
|
142
173
|
* creation if a cluster of the same name already exists. Defaults to the
|
|
@@ -279,8 +310,8 @@ export const azDelete = (templatePath: string, opts?: Record<string, unknown>):
|
|
|
279
310
|
* Deploy a built CloudFormation template by calling the CloudFormation API
|
|
280
311
|
* directly (create-or-update + poll) — the direct twin of {@link azApply} /
|
|
281
312
|
* {@link gcpApply} for AWS, targeting a local Floci emulator or real AWS by
|
|
282
|
-
* endpoint override. Speaks the CFN API over HTTP rather than shelling `aws`
|
|
283
|
-
*
|
|
313
|
+
* endpoint override. Speaks the CFN API over HTTP rather than shelling `aws` —
|
|
314
|
+
* `nativeApply({ target: "cloudformation" })` routes here too (#1449). Provided by
|
|
284
315
|
* the aws lexicon; loaded when the project lists `aws`. Defaults to the
|
|
285
316
|
* `longInfra` profile. `opts` requires `stackName`; accepts `endpoint`, `region`,
|
|
286
317
|
* `capabilities`, `timeoutMs`, `intervalMs`.
|
|
@@ -297,13 +328,15 @@ export const awsDelete = (templatePath: string, opts?: Record<string, unknown>):
|
|
|
297
328
|
};
|
|
298
329
|
|
|
299
330
|
/**
|
|
300
|
-
* Apply chant's built GCP (CNRM) resources directly to
|
|
331
|
+
* Apply chant's built GCP (CNRM) resources directly to their GCP REST APIs,
|
|
301
332
|
* targeting a local floci-gcp emulator or real GCP by endpoint override — the
|
|
302
|
-
* native GCP applier (#706 starter #711)
|
|
303
|
-
* Defaults to the `longInfra` profile (override via
|
|
333
|
+
* native GCP applier (#706 starter #711). `nativeApply({ target: "gcp" })`
|
|
334
|
+
* routes here too (#1449). Defaults to the `longInfra` profile (override via
|
|
335
|
+
* `opts.profile`).
|
|
304
336
|
*
|
|
305
|
-
* `opts` accepts `endpoint` (default `
|
|
306
|
-
* `project` (default `GOOGLE_CLOUD_PROJECT` env / the CNRM
|
|
337
|
+
* `opts` accepts `endpoint` (default `GCP_ENDPOINT_URL` env / each kind's
|
|
338
|
+
* real-GCP host) and `project` (default `GOOGLE_CLOUD_PROJECT` env / the CNRM
|
|
339
|
+
* project-id annotation).
|
|
307
340
|
*/
|
|
308
341
|
export const gcpApply = (manifestPath: string, opts?: Record<string, unknown>): ActivityStep => {
|
|
309
342
|
const { args, profile } = takeProfile(opts);
|
package/src/op/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForStack, waitForReady,
|
|
2
|
-
gitlabPipeline, lifecycleSnapshot, shell, teardown, k3dUp, k3dDown, flociUp, flociDown,
|
|
2
|
+
gitlabPipeline, lifecycleSnapshot, shell, teardown, envTeardown, k3dUp, k3dDown, flociUp, flociDown,
|
|
3
3
|
flociAzUp, flociAzDown, flociGcpUp, flociGcpDown, httpCheck,
|
|
4
4
|
azGroupEnsure, azGroupDelete, azApply, azDelete, awsApply, awsDelete, gcpApply, gcpDelete, policyGate,
|
|
5
5
|
spriteCreate, spriteExec, spriteCheckpoint, spriteRestore, listCheckpoints, spriteDestroy,
|
package/src/ownership.test.ts
CHANGED
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
OWNERSHIP_MANAGED_BY_VALUE,
|
|
10
10
|
type ChannelKeys,
|
|
11
11
|
} from "./ownership";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveOwnershipMarker,
|
|
14
|
+
resolveOwnershipEnv,
|
|
15
|
+
resolveOwnershipStack,
|
|
16
|
+
ownershipEnvDisagreement,
|
|
17
|
+
} from "./config";
|
|
13
18
|
|
|
14
19
|
// A stand-in for a lexicon-provided convention (e.g. AWS's colon keys), to
|
|
15
20
|
// prove the core helpers are generic over any `ChannelKeys` — the per-provider
|
|
@@ -105,3 +110,60 @@ describe("resolveOwnershipMarker (config opt-in)", () => {
|
|
|
105
110
|
expect(resolveOwnershipMarker({ ownership: { stack: "billing", enabled: false } })).toBeUndefined();
|
|
106
111
|
});
|
|
107
112
|
});
|
|
113
|
+
|
|
114
|
+
describe("ownership.env as a build-parameter reference (#1396)", () => {
|
|
115
|
+
const config = {
|
|
116
|
+
ownership: { stack: "fountain", env: { param: "env" } },
|
|
117
|
+
buildParams: { env: { type: "string" as const, default: "dev", env: "FOUNTAIN_ENV" } },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
test("takes the resolved parameter value — --param env=prod drives the marker", () => {
|
|
121
|
+
expect(resolveOwnershipMarker(config, [{ name: "env", value: "prod", source: "cli" }])).toEqual({
|
|
122
|
+
stack: "fountain",
|
|
123
|
+
env: "prod",
|
|
124
|
+
});
|
|
125
|
+
expect(resolveOwnershipMarker(config, [{ name: "env", value: "dev", source: "default" }])).toEqual({
|
|
126
|
+
stack: "fountain",
|
|
127
|
+
env: "dev",
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a reference to an undeclared parameter is an error, not an env-less marker", () => {
|
|
132
|
+
expect(() => resolveOwnershipMarker({ ownership: { stack: "s", env: { param: "env" } } }, [])).toThrow(
|
|
133
|
+
/does not declare/,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a reference to a parameter that resolved to nothing is an error", () => {
|
|
138
|
+
expect(() => resolveOwnershipMarker(config, [])).toThrow(
|
|
139
|
+
/resolved to no value.*--param env=<value>.*FOUNTAIN_ENV/,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("the stack-only resolver needs no parameters", () => {
|
|
144
|
+
expect(resolveOwnershipStack(config)).toBe("fountain");
|
|
145
|
+
expect(resolveOwnershipStack({ ownership: { stack: "s", enabled: false } })).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a literal env is returned untouched, with or without parameters", () => {
|
|
149
|
+
expect(resolveOwnershipEnv({ ownership: { stack: "s", env: "prod" } }, undefined)).toBe("prod");
|
|
150
|
+
expect(resolveOwnershipEnv({ ownership: { stack: "s" } }, [])).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a literal env disagreeing with an env parameter is reported", () => {
|
|
154
|
+
const literal = {
|
|
155
|
+
ownership: { stack: "fountain", env: "dev" },
|
|
156
|
+
buildParams: { env: { type: "string" as const, default: "dev" } },
|
|
157
|
+
};
|
|
158
|
+
expect(ownershipEnvDisagreement(literal, [{ name: "env", value: "dev", source: "default" }])).toBeUndefined();
|
|
159
|
+
expect(ownershipEnvDisagreement(literal, [{ name: "env", value: "prod", source: "cli" }])).toMatch(
|
|
160
|
+
/ownership\.env is "dev" but the env build parameter resolved to "prod" \(cli\)/,
|
|
161
|
+
);
|
|
162
|
+
// A reference never disagrees — there is one source.
|
|
163
|
+
expect(ownershipEnvDisagreement(config, [{ name: "env", value: "prod", source: "cli" }])).toBeUndefined();
|
|
164
|
+
// Marking off — nothing is stamped, nothing to disagree with.
|
|
165
|
+
expect(
|
|
166
|
+
ownershipEnvDisagreement({ ownership: { env: "dev" } }, [{ name: "env", value: "prod", source: "cli" }]),
|
|
167
|
+
).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
});
|