@shortlink-org/portolan 0.2.4 → 0.4.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 +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
package/scripts/history.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// When each file was first committed and last changed, read by the host for a
|
|
2
|
-
// plugin that asks (portolan.0007)
|
|
2
|
+
// plugin that asks (portolan.0007), and for the provenance of every catalog
|
|
3
|
+
// source (portolan.0010).
|
|
3
4
|
//
|
|
4
5
|
// One `git log` over the whole checkout, walked oldest first with renames
|
|
5
6
|
// followed, gives every path its first commit and its last. A plugin used to
|
|
@@ -8,8 +9,8 @@
|
|
|
8
9
|
// reads it once and hands each step the part under its root.
|
|
9
10
|
|
|
10
11
|
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { realpathSync } from "node:fs";
|
|
12
|
-
import { relative, resolve, sep } from "node:path";
|
|
12
|
+
import { realpathSync, statSync } from "node:fs";
|
|
13
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
13
14
|
|
|
14
15
|
const RECORD = "\x1e";
|
|
15
16
|
const FIELD = "\x1f";
|
|
@@ -17,6 +18,188 @@ const FIELD = "\x1f";
|
|
|
17
18
|
/** Per repository, per process: the history does not change during a run. */
|
|
18
19
|
const cache = new Map();
|
|
19
20
|
|
|
21
|
+
/** Repositories already told they are shallow; it is said once. */
|
|
22
|
+
const warnedShallow = new Set();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* When each of `paths` last changed, as the history says: the commit that
|
|
26
|
+
* last touched the file, and that commit's date.
|
|
27
|
+
*
|
|
28
|
+
* This is where a source's provenance comes from (portolan.0010). It is read
|
|
29
|
+
* off the checkout that holds the file and never written into the file, so a
|
|
30
|
+
* fragment is content and nothing else, and a regeneration that changes no
|
|
31
|
+
* fact changes no byte. The stamp names the commit the change landed in,
|
|
32
|
+
* which a stamp written into the file never could: that one was the parent
|
|
33
|
+
* at best, and it took a second commit to catch up.
|
|
34
|
+
*
|
|
35
|
+
* A file with uncommitted changes, or one git does not track, is
|
|
36
|
+
* `uncommitted` and dated by its mtime - an observation of the working tree,
|
|
37
|
+
* shown and never committed. A path outside any repository is the same. A
|
|
38
|
+
* shallow clone makes every path look as though it changed in the one commit
|
|
39
|
+
* that was fetched; that is said once, and the stamps are what the truncated
|
|
40
|
+
* history says, which is at least the checkout.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} workspace
|
|
43
|
+
* @param {string[]} paths relative to `workspace`, forward slashes
|
|
44
|
+
* @returns {Map<string, {commit: string, generatedAt: string}>}
|
|
45
|
+
*/
|
|
46
|
+
export function stampsFor(workspace, paths) {
|
|
47
|
+
const workspaceReal = realpathSync(resolve(workspace));
|
|
48
|
+
const stamps = new Map();
|
|
49
|
+
const repoOfDir = new Map();
|
|
50
|
+
const byRepo = new Map();
|
|
51
|
+
|
|
52
|
+
for (const path of paths) {
|
|
53
|
+
const absolute = resolve(workspaceReal, path);
|
|
54
|
+
const dir = dirname(absolute);
|
|
55
|
+
if (!repoOfDir.has(dir)) repoOfDir.set(dir, repositoryOf(dir));
|
|
56
|
+
const repo = repoOfDir.get(dir);
|
|
57
|
+
if (!repo) {
|
|
58
|
+
stamps.set(path, uncommitted(absolute));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!byRepo.has(repo)) byRepo.set(repo, []);
|
|
62
|
+
byRepo.get(repo).push({ path, absolute, key: toPosix(relative(repo, absolute)) });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [repo, files] of byRepo) {
|
|
66
|
+
warnIfShallow(repo);
|
|
67
|
+
const history = fileHistory(repo);
|
|
68
|
+
const dirty = dirtyIn(repo);
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const entry = history.get(file.key);
|
|
71
|
+
const last = entry?.revised ?? entry?.created;
|
|
72
|
+
stamps.set(
|
|
73
|
+
file.path,
|
|
74
|
+
!last || dirty.has(file.key)
|
|
75
|
+
? uncommitted(file.absolute)
|
|
76
|
+
: { commit: last.commit.slice(0, 7), generatedAt: last.date },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return stamps;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The commit that last touched any of `paths`, and its date, or null when
|
|
86
|
+
* none of them has been committed. For a step's output this is when it was
|
|
87
|
+
* last generated: the history remembers it, so nothing has to write it down
|
|
88
|
+
* anywhere else (portolan.0010).
|
|
89
|
+
*
|
|
90
|
+
* @param {string} workspace
|
|
91
|
+
* @param {string[]} paths relative to `workspace`
|
|
92
|
+
* @returns {{commit: string, date: string} | null}
|
|
93
|
+
*/
|
|
94
|
+
export function lastCommitTouching(workspace, paths) {
|
|
95
|
+
const { repo, keys } = inRepo(workspace, paths);
|
|
96
|
+
if (!repo || keys.length === 0) return null;
|
|
97
|
+
const answer = git(repo, ["log", "-1", "--format=%H %cI", "--", ...keys]);
|
|
98
|
+
if (!answer) return null;
|
|
99
|
+
const [sha, date] = answer.split(" ");
|
|
100
|
+
// Seven characters, the width every stamp has, so the two read alike.
|
|
101
|
+
return sha && date ? { commit: sha.slice(0, 7), date } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Which files under `paths` differ from what `commit` held, working tree
|
|
106
|
+
* included: modified, added, deleted and untracked, less anything under
|
|
107
|
+
* `excludes`. Paths in and out are relative to `workspace`.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} workspace
|
|
110
|
+
* @param {string} commit
|
|
111
|
+
* @param {string[]} paths
|
|
112
|
+
* @param {string[]} [excludes]
|
|
113
|
+
* @returns {string[]}
|
|
114
|
+
*/
|
|
115
|
+
export function changedSince(workspace, commit, paths, excludes = []) {
|
|
116
|
+
const { repo, keys, toWorkspace } = inRepo(workspace, paths);
|
|
117
|
+
if (!repo || keys.length === 0) return [];
|
|
118
|
+
const spec = [...keys, ...inRepo(workspace, excludes).keys.map((key) => `:(exclude)${key}`)];
|
|
119
|
+
const tracked = git(repo, ["diff", "--name-only", commit, "--", ...spec]);
|
|
120
|
+
const untracked = git(repo, ["ls-files", "--others", "--exclude-standard", "--", ...spec]);
|
|
121
|
+
const changed = new Set();
|
|
122
|
+
for (const line of `${tracked}\n${untracked}`.split("\n")) {
|
|
123
|
+
if (line) changed.add(toWorkspace(line));
|
|
124
|
+
}
|
|
125
|
+
return [...changed].sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The text of `path` as `commit` held it, or "" when the commit did not hold
|
|
130
|
+
* it or there is no history to ask.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} workspace
|
|
133
|
+
* @param {string} commit
|
|
134
|
+
* @param {string} path relative to `workspace`
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
export function fileAt(workspace, commit, path) {
|
|
138
|
+
const { repo, keys } = inRepo(workspace, [path]);
|
|
139
|
+
if (!repo || keys.length === 0) return "";
|
|
140
|
+
return git(repo, ["show", `${commit}:${keys[0]}`]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The paths as the workspace's repository spells them, and the way back. */
|
|
144
|
+
function inRepo(workspace, paths) {
|
|
145
|
+
const workspaceReal = realpathSync(resolve(workspace));
|
|
146
|
+
const repo = repositoryOf(workspaceReal);
|
|
147
|
+
if (!repo) return { repo: "", keys: [], toWorkspace: (line) => line };
|
|
148
|
+
const keys = [];
|
|
149
|
+
for (const path of paths) {
|
|
150
|
+
const key = toPosix(relative(repo, resolve(workspaceReal, path)));
|
|
151
|
+
if (key.startsWith("..")) continue;
|
|
152
|
+
keys.push(key || ".");
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
repo,
|
|
156
|
+
keys,
|
|
157
|
+
toWorkspace: (line) => toPosix(relative(workspaceReal, resolve(repo, line))),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function uncommitted(absolute) {
|
|
162
|
+
let generatedAt = "";
|
|
163
|
+
try {
|
|
164
|
+
generatedAt = statSync(absolute).mtime.toISOString();
|
|
165
|
+
} catch {
|
|
166
|
+
// Gone, or never there: undated rather than dated by a guess.
|
|
167
|
+
}
|
|
168
|
+
return { commit: "uncommitted", generatedAt };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Every path with a change the history does not hold yet, relative to the repository. */
|
|
172
|
+
function dirtyIn(repo) {
|
|
173
|
+
const dirty = new Set();
|
|
174
|
+
// Read raw: an entry begins with two status letters, and a space is one of
|
|
175
|
+
// them, which the trimming reader would eat off the first line.
|
|
176
|
+
let status = "";
|
|
177
|
+
try {
|
|
178
|
+
status = execFileSync("git", ["-C", repo, "status", "--porcelain", "-z", "--no-renames", "--untracked-files=all"], {
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
181
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
return dirty;
|
|
185
|
+
}
|
|
186
|
+
for (const entry of status.split("\0")) {
|
|
187
|
+
if (entry.length > 3) dirty.add(entry.slice(3));
|
|
188
|
+
}
|
|
189
|
+
return dirty;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function warnIfShallow(repo) {
|
|
193
|
+
if (warnedShallow.has(repo)) return;
|
|
194
|
+
warnedShallow.add(repo);
|
|
195
|
+
if (git(repo, ["rev-parse", "--is-shallow-repository"]) !== "true") return;
|
|
196
|
+
console.warn(
|
|
197
|
+
`${repo} is a shallow clone, where every source looks as though it last changed in ` +
|
|
198
|
+
"the one commit that was fetched. Fetch the full history (git fetch --unshallow, " +
|
|
199
|
+
"or actions/checkout with fetch-depth: 0) for the provenance to say anything.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
20
203
|
/**
|
|
21
204
|
* History for every file under `root`, keyed by the path a plugin would name
|
|
22
205
|
* it - relative to `workspace`, forward slashes. `undefined` when the root is
|
package/scripts/history.test.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { afterEach, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { fileHistory, forgetHistory, historyFor, parseLog } from "./history.mjs";
|
|
7
|
+
import { changedSince, fileHistory, forgetHistory, historyFor, lastCommitTouching, parseLog, stampsFor } from "./history.mjs";
|
|
8
8
|
|
|
9
9
|
const created = [];
|
|
10
10
|
afterEach(() => {
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// fetch-argocd, run by the host: an Argo CD API on one side, a snapshot of
|
|
2
|
+
// where the estate's services run committed to this repository on the other.
|
|
3
|
+
//
|
|
4
|
+
// portolan.0011 reads the names a service answers on and dials, from the
|
|
5
|
+
// manifests and from the cluster, and nothing else; neither says which
|
|
6
|
+
// revision of which repository stands in which cluster and namespace, under
|
|
7
|
+
// which name. Only the thing that deploys knows that (portolan.0012). It is
|
|
8
|
+
// not in any tree, so it cannot be an extractor's, and it is read over a
|
|
9
|
+
// socket with a credential, so it is the host's (portolan.0008).
|
|
10
|
+
//
|
|
11
|
+
// The contract is the plugin's all the same: the snapshot is named, never
|
|
12
|
+
// written, so the host writes it, it gets a manifest entry, is compared by
|
|
13
|
+
// `gen:check` like any other generated file, and is removed when the step
|
|
14
|
+
// stops naming it. Two files land in the step's out. `argocd.apps.json` is
|
|
15
|
+
// a catalog fragment - the deployments, one per Application, nothing else.
|
|
16
|
+
// `argocd.lock.json` is for the next run of this step: the server, the
|
|
17
|
+
// applications and the fragment's digest, which is what lets an offline run
|
|
18
|
+
// replay the committed snapshot and refuse one edited by hand.
|
|
19
|
+
//
|
|
20
|
+
// There is no pin. An Application has no immutable commit the way a module
|
|
21
|
+
// or a repository has; the snapshot IS the fact, and it moves when a deploy
|
|
22
|
+
// moves it. So an online run after a deploy changes the committed snapshot,
|
|
23
|
+
// and that diff is the review worth having: what went where, in one pull
|
|
24
|
+
// request. What is kept is what a deploy changes - the revision, the images,
|
|
25
|
+
// the destination. What is not kept is what changes without one: health,
|
|
26
|
+
// sync state, the time of the last operation. A page that said "healthy"
|
|
27
|
+
// yesterday and was not regenerated since is a page that lies, and a build
|
|
28
|
+
// that reads a clock is not reproducible (portolan.0010).
|
|
29
|
+
//
|
|
30
|
+
// In CI, or with PORTOLAN_OFFLINE=1, the step replays the committed
|
|
31
|
+
// snapshot, checks it against its lock and emits an identical file list, so
|
|
32
|
+
// a fork's pull request needs no token and Argo CD being down cannot turn
|
|
33
|
+
// the tree red.
|
|
34
|
+
//
|
|
35
|
+
// No token in the manifest, ever: portolan.json is committed. The credential
|
|
36
|
+
// comes from ARGOCD_AUTH_TOKEN, the variable the argocd CLI reads, and it
|
|
37
|
+
// changes whether the fetch succeeds, never what the fetch says.
|
|
38
|
+
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
|
|
43
|
+
import optionsSchema from "./fetch-argocd.options.json" with { type: "json" };
|
|
44
|
+
import { webRepo } from "./fetch-git.mjs";
|
|
45
|
+
|
|
46
|
+
export const LOCK_NAME = "argocd.lock.json";
|
|
47
|
+
export const FRAGMENT_NAME = "argocd.apps.json";
|
|
48
|
+
export const TOKEN_ENV = "ARGOCD_AUTH_TOKEN";
|
|
49
|
+
export const OFFLINE_ENV = "PORTOLAN_OFFLINE";
|
|
50
|
+
|
|
51
|
+
/** The one call: every Application the token can list, narrowed by query. */
|
|
52
|
+
export const LIST_PATH = "/api/v1/applications";
|
|
53
|
+
|
|
54
|
+
/** The label that names an environment when the manifest does not say. */
|
|
55
|
+
export const DEFAULT_ENVIRONMENT_LABEL = "env";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The labels that name the service an Application deploys, when the
|
|
59
|
+
* manifest does not say: the Kubernetes recommended ones, the same
|
|
60
|
+
* fetch-k8s reads off a workload. An ApplicationSet that stamps them on
|
|
61
|
+
* every Application it makes is what lets a GitOps repository - where the
|
|
62
|
+
* path points at an overlay, not at the service - still place each
|
|
63
|
+
* Application on its service.
|
|
64
|
+
*/
|
|
65
|
+
export const DEFAULT_LABELS = { context: "app.kubernetes.io/part-of", service: "app.kubernetes.io/name" };
|
|
66
|
+
|
|
67
|
+
/** How Argo CD names the cluster it runs in, and how the catalog does. */
|
|
68
|
+
const IN_CLUSTER_SERVER = "https://kubernetes.default.svc";
|
|
69
|
+
const IN_CLUSTER_NAME = "in-cluster";
|
|
70
|
+
|
|
71
|
+
export function describe() {
|
|
72
|
+
return {
|
|
73
|
+
name: "fetch-argocd",
|
|
74
|
+
summary: "Reads the applications an Argo CD server manages into a snapshot of where each service runs - cluster, namespace, revision, images - with a lock beside it.",
|
|
75
|
+
category: "sources",
|
|
76
|
+
phases: ["extract"],
|
|
77
|
+
options: optionsSchema,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The step. Reads nothing out of the tree being described; its `cache` is
|
|
83
|
+
* repo-relative and everything else is in the options.
|
|
84
|
+
*
|
|
85
|
+
* The rules are fetch-bsr's: a failed fetch falls back to the committed
|
|
86
|
+
* snapshot with a warning, and a failed fetch with no snapshot is an ERROR,
|
|
87
|
+
* never a short file list, because the host deletes files a step stops
|
|
88
|
+
* naming.
|
|
89
|
+
*
|
|
90
|
+
* @param {{options?: object}} request
|
|
91
|
+
* @param {{env?: NodeJS.ProcessEnv, fetch?: typeof fetch}} [io]
|
|
92
|
+
*/
|
|
93
|
+
export async function run(request, { env = process.env, fetch: fetchFn = globalThis.fetch } = {}) {
|
|
94
|
+
const options = request.options ?? {};
|
|
95
|
+
const server = String(options.server ?? "").trim();
|
|
96
|
+
if (!server) throw new Error("no server: name the Argo CD API server in the manifest");
|
|
97
|
+
if (!options.cache) {
|
|
98
|
+
throw new Error("no cache directory: set `cache` to the same path as the step's `out`, so an offline run can replay what the last online one wrote");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const out = new Builder();
|
|
102
|
+
if (offline(env)) {
|
|
103
|
+
emitCached(out, options.cache, "offline");
|
|
104
|
+
return out.response();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let deployments;
|
|
108
|
+
try {
|
|
109
|
+
deployments = await live(server, options, env, fetchFn);
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
try {
|
|
112
|
+
emitCached(out, options.cache, cause.message);
|
|
113
|
+
} catch (cacheCause) {
|
|
114
|
+
throw new Error(`${server} could not be read (${cause.message}) and there is no usable snapshot in the tree (${cacheCause.message})`);
|
|
115
|
+
}
|
|
116
|
+
return out.response();
|
|
117
|
+
}
|
|
118
|
+
emitFetched(out, server, deployments);
|
|
119
|
+
return out.response();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function offline(env = process.env) {
|
|
123
|
+
if (String(env[OFFLINE_ENV] ?? "").trim()) return true;
|
|
124
|
+
return !["", "0", "false"].includes(String(env.CI ?? "").trim().toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A credential, or "". The argocd CLI reads the same variable. */
|
|
128
|
+
export function token(env = process.env) {
|
|
129
|
+
return String(env[TOKEN_ENV] ?? "").trim();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The server as a base URL: https unless the manifest says otherwise. */
|
|
133
|
+
export function baseUrl(server) {
|
|
134
|
+
const value = String(server ?? "").trim().replace(/\/+$/, "");
|
|
135
|
+
return /^https?:\/\//.test(value) ? value : `https://${value}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One Application, reduced to what a deploy changes.
|
|
140
|
+
*
|
|
141
|
+
* Everything the wire knows is looked at here and nowhere else. What comes
|
|
142
|
+
* out is names and revisions: no parameter, no value, no status that moves
|
|
143
|
+
* without a commit. A Helm parameter or a plugin env is a value and stays
|
|
144
|
+
* behind for the reason portolan.0011 gives.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} app an item of the list answer
|
|
147
|
+
* @param {string} base the server's base URL, for the link
|
|
148
|
+
* @param {string} environmentLabel
|
|
149
|
+
* @param {{context?: string, service?: string}} serviceLabels
|
|
150
|
+
*/
|
|
151
|
+
export function deploymentOf(app, base, environmentLabel = DEFAULT_ENVIRONMENT_LABEL, serviceLabels = DEFAULT_LABELS) {
|
|
152
|
+
const metadata = app?.metadata ?? {};
|
|
153
|
+
const spec = app?.spec ?? {};
|
|
154
|
+
const status = app?.status ?? {};
|
|
155
|
+
const name = String(metadata.name ?? "").trim();
|
|
156
|
+
if (!name) throw new Error("an application has no name");
|
|
157
|
+
const appNamespace = String(metadata.namespace ?? "argocd").trim() || "argocd";
|
|
158
|
+
|
|
159
|
+
// A multi-source application deploys the source that carries a path (its
|
|
160
|
+
// manifests) and reads values out of the others; the path is the one
|
|
161
|
+
// whose revision a reader wants to follow.
|
|
162
|
+
const sources = Array.isArray(spec.sources) && spec.sources.length ? spec.sources : spec.source ? [spec.source] : [];
|
|
163
|
+
const sourceIndex = Math.max(0, sources.findIndex((source) => String(source?.path ?? "").trim()));
|
|
164
|
+
const source = sources[sourceIndex] ?? {};
|
|
165
|
+
const revisions = Array.isArray(status.sync?.revisions) ? status.sync.revisions : [];
|
|
166
|
+
const revision = String(revisions[sourceIndex] ?? status.sync?.revision ?? "").trim();
|
|
167
|
+
|
|
168
|
+
const destination = spec.destination ?? {};
|
|
169
|
+
const server = String(destination.server ?? "").trim();
|
|
170
|
+
const cluster = String(destination.name ?? "").trim() || (server === IN_CLUSTER_SERVER ? IN_CLUSTER_NAME : server);
|
|
171
|
+
const labels = metadata.labels ?? {};
|
|
172
|
+
const environment = String(labels[environmentLabel] ?? "").trim() || cluster;
|
|
173
|
+
|
|
174
|
+
const record = {
|
|
175
|
+
id: `${appNamespace}/${name}`,
|
|
176
|
+
name,
|
|
177
|
+
project: String(spec.project ?? "default").trim() || "default",
|
|
178
|
+
environment,
|
|
179
|
+
cluster,
|
|
180
|
+
namespace: String(destination.namespace ?? "").trim(),
|
|
181
|
+
repo: webRepo(source.repoURL ?? ""),
|
|
182
|
+
path: String(source.path ?? "").trim().replace(/^\.?\/+|\/+$/g, ""),
|
|
183
|
+
targetRevision: String(source.targetRevision ?? "").trim(),
|
|
184
|
+
revision,
|
|
185
|
+
tool: toolOf(status.sourceType ?? status.sourceTypes?.[sourceIndex], ...sources),
|
|
186
|
+
url: `${base}/applications/${encodeURIComponent(appNamespace)}/${encodeURIComponent(name)}`,
|
|
187
|
+
// Who said so: the deployer. A row extract-argocd reads out of the
|
|
188
|
+
// GitOps tree says "manifest", and the merge lays the two over each
|
|
189
|
+
// other (portolan.0013).
|
|
190
|
+
basis: "api",
|
|
191
|
+
};
|
|
192
|
+
// The service, when the labels say: `<context>.<service>`, the id a
|
|
193
|
+
// service carries in the catalog. Both labels or nothing - a name without
|
|
194
|
+
// a context is a word, and the catalog does not place words.
|
|
195
|
+
const contextSlug = String(labels[serviceLabels.context ?? DEFAULT_LABELS.context] ?? "").trim();
|
|
196
|
+
const serviceSlug = String(labels[serviceLabels.service ?? DEFAULT_LABELS.service] ?? "").trim();
|
|
197
|
+
if (contextSlug && serviceSlug) record.service = `${contextSlug}.${serviceSlug}`;
|
|
198
|
+
const chart = String(source.chart ?? "").trim();
|
|
199
|
+
if (chart) record.chart = chart;
|
|
200
|
+
const images = [...new Set((status.summary?.images ?? []).map((image) => String(image).trim()).filter(Boolean))].sort();
|
|
201
|
+
if (images.length) record.images = images;
|
|
202
|
+
return record;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `Helm`, `Kustomize`, `Directory`, `Plugin` as Argo CD says it, lowercased;
|
|
207
|
+
* inferred from the sources when the status has not said yet. A multi-source
|
|
208
|
+
* application says it on the chart, not on the values it reads beside it.
|
|
209
|
+
*/
|
|
210
|
+
export function toolOf(sourceType, ...sources) {
|
|
211
|
+
const said = String(sourceType ?? "").trim().toLowerCase();
|
|
212
|
+
if (said) return said;
|
|
213
|
+
for (const source of sources) {
|
|
214
|
+
if (source?.chart || source?.helm) return "helm";
|
|
215
|
+
if (source?.kustomize) return "kustomize";
|
|
216
|
+
if (source?.plugin) return "plugin";
|
|
217
|
+
if (source?.directory) return "directory";
|
|
218
|
+
}
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The fragment: a catalog holding deployments and nothing else. */
|
|
223
|
+
export function fragment(deployments) {
|
|
224
|
+
const sorted = [...deployments].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
225
|
+
return `${JSON.stringify({ contexts: [], defs: {}, flows: [], adrs: [], deployments: sorted }, null, 2)}\n`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The lock, written the way every generated file here is written. */
|
|
229
|
+
export function encodeLock(entry) {
|
|
230
|
+
return `${JSON.stringify({ server: entry.server, applications: [...entry.applications].sort(), sha256: entry.sha256 }, null, 2)}\n`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function digestOf(contents) {
|
|
234
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function live(server, options, env, fetchFn) {
|
|
238
|
+
const base = baseUrl(server);
|
|
239
|
+
const query = new URLSearchParams();
|
|
240
|
+
for (const project of Array.isArray(options.projects) ? options.projects : []) query.append("projects", String(project));
|
|
241
|
+
if (options.selector) query.set("selector", String(options.selector));
|
|
242
|
+
const url = `${base}${LIST_PATH}${query.size ? `?${query}` : ""}`;
|
|
243
|
+
|
|
244
|
+
const headers = { Accept: "application/json" };
|
|
245
|
+
const credential = token(env);
|
|
246
|
+
if (credential) headers.Authorization = `Bearer ${credential}`;
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
// A timeout rather than none: a step that hangs holds up the whole run.
|
|
250
|
+
response = await fetchFn(url, { method: "GET", headers, signal: AbortSignal.timeout(60_000) });
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
throw new Error(`${LIST_PATH}: ${cause.cause?.message ?? cause.message}`);
|
|
253
|
+
}
|
|
254
|
+
const raw = await response.text();
|
|
255
|
+
if (!response.ok) throw new Error(`${LIST_PATH}: ${apiError(response.status, raw)}`);
|
|
256
|
+
let answer;
|
|
257
|
+
try {
|
|
258
|
+
answer = JSON.parse(raw);
|
|
259
|
+
} catch {
|
|
260
|
+
throw new Error(`${LIST_PATH}: the answer is not JSON`);
|
|
261
|
+
}
|
|
262
|
+
const items = Array.isArray(answer?.items) ? answer.items : [];
|
|
263
|
+
if (items.length === 0) throw new Error(`${server} lists no applications; check the projects and selector in the manifest, and what the token may see`);
|
|
264
|
+
const label = String(options.environmentLabel ?? "").trim() || DEFAULT_ENVIRONMENT_LABEL;
|
|
265
|
+
const serviceLabels = { ...DEFAULT_LABELS, ...(options.labels ?? {}) };
|
|
266
|
+
return items.map((item) => deploymentOf(item, base, label, serviceLabels));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The error body Argo CD sends, or the status when the body is not one. */
|
|
270
|
+
function apiError(status, raw) {
|
|
271
|
+
try {
|
|
272
|
+
const wire = JSON.parse(raw);
|
|
273
|
+
if (wire?.message) return wire.message;
|
|
274
|
+
if (wire?.error) return wire.error;
|
|
275
|
+
} catch {
|
|
276
|
+
// Not an error body.
|
|
277
|
+
}
|
|
278
|
+
return `http ${status}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function emitFetched(out, server, deployments) {
|
|
282
|
+
const contents = fragment(deployments);
|
|
283
|
+
out.file(FRAGMENT_NAME, contents);
|
|
284
|
+
out.file(LOCK_NAME, encodeLock({ server, applications: deployments.map((d) => d.id), sha256: digestOf(contents) }));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function emitCached(out, cache, why) {
|
|
288
|
+
const held = replay(cache);
|
|
289
|
+
out.file(FRAGMENT_NAME, held.contents);
|
|
290
|
+
out.file(LOCK_NAME, encodeLock(held.lock));
|
|
291
|
+
out.warn(held.lock.server, `not fetched (${why}); the snapshot committed in this repository is used unchanged`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function replay(dir) {
|
|
295
|
+
const lockPath = join(dir, LOCK_NAME);
|
|
296
|
+
let raw;
|
|
297
|
+
try {
|
|
298
|
+
raw = readFileSync(lockPath, "utf8");
|
|
299
|
+
} catch (cause) {
|
|
300
|
+
if (cause.code === "ENOENT") throw new Error(`no ${LOCK_NAME} in ${dir.split("\\").join("/")}`);
|
|
301
|
+
throw cause;
|
|
302
|
+
}
|
|
303
|
+
let lock;
|
|
304
|
+
try {
|
|
305
|
+
lock = JSON.parse(raw);
|
|
306
|
+
} catch (cause) {
|
|
307
|
+
throw new Error(`${lockPath}: ${cause.message}`);
|
|
308
|
+
}
|
|
309
|
+
let contents;
|
|
310
|
+
try {
|
|
311
|
+
contents = readFileSync(join(dir, FRAGMENT_NAME), "utf8");
|
|
312
|
+
} catch (cause) {
|
|
313
|
+
if (cause.code === "ENOENT") throw new Error(`${FRAGMENT_NAME} is in the lock but not on disk`);
|
|
314
|
+
throw cause;
|
|
315
|
+
}
|
|
316
|
+
if (digestOf(contents) !== lock.sha256) throw new Error(`${FRAGMENT_NAME} does not match its digest; the snapshot was edited by hand`);
|
|
317
|
+
return {
|
|
318
|
+
lock: { server: String(lock.server ?? ""), applications: Array.isArray(lock.applications) ? lock.applications : [], sha256: String(lock.sha256 ?? "") },
|
|
319
|
+
contents,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
class Builder {
|
|
324
|
+
files = [];
|
|
325
|
+
warnings = [];
|
|
326
|
+
|
|
327
|
+
file(name, contents) {
|
|
328
|
+
this.files.push({ name, contents });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
warn(ref, message) {
|
|
332
|
+
this.warnings.push({ severity: "warning", message, ref });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
response() {
|
|
336
|
+
return { files: this.files, warnings: this.warnings };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"required": ["server", "cache"],
|
|
5
|
+
"properties": {
|
|
6
|
+
"server": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"description": "The Argo CD API server: a host, or a URL when it is not https. The token comes from ARGOCD_AUTH_TOKEN, never from here.",
|
|
9
|
+
"examples": ["argocd.example.com"]
|
|
10
|
+
},
|
|
11
|
+
"projects": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": { "type": "string", "minLength": 1 },
|
|
14
|
+
"description": "Argo CD projects to read applications from. Left out, every application the token can list.",
|
|
15
|
+
"examples": [["shop", "platform"]]
|
|
16
|
+
},
|
|
17
|
+
"selector": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "A label selector narrowing the applications, as `argocd app list -l` takes one.",
|
|
20
|
+
"examples": ["team=shop"]
|
|
21
|
+
},
|
|
22
|
+
"environmentLabel": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"default": "env",
|
|
25
|
+
"description": "The application label that names the environment. An application without it is placed by the cluster it deploys to."
|
|
26
|
+
},
|
|
27
|
+
"labels": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"description": "Which application labels name the service it deploys. The defaults are the Kubernetes recommended labels, the same ones fetch-k8s reads off a workload; an estate that labels differently names its own keys here. An application without them is placed by the repository and directory it deploys from.",
|
|
31
|
+
"properties": {
|
|
32
|
+
"context": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "The label whose value is the bounded context's slug.",
|
|
35
|
+
"default": "app.kubernetes.io/part-of"
|
|
36
|
+
},
|
|
37
|
+
"service": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "The label whose value is the service's slug.",
|
|
40
|
+
"default": "app.kubernetes.io/name"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"cache": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Where the last run's snapshot was written, repo-relative: the same path as the step's out. An offline run replays what is there and checks it against the lock beside it."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|