@shortlink-org/portolan 0.3.0 → 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 +151 -6
- package/catalog/model.go +147 -0
- package/catalog/roundtrip_test.go +7 -0
- package/cli/portolan.mjs +6 -1
- package/go.mod +7 -0
- package/go.sum +20 -6
- package/package.json +10 -9
- package/plugin/describe.go +38 -0
- package/plugins/README.md +296 -5
- package/plugins/extract-argocd/options.schema.json +44 -0
- 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 +13 -2
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +2 -1
- package/plugins/extract-django/extract_test.py +13 -0
- package/plugins/extract-django/lifecycle.py +2 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +5 -4
- package/plugins/extract-java/build/org/portolan/extract/Main.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/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/main.ts +1 -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/verify-otel/options.schema.json +12 -0
- package/portolan.json +130 -3
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +880 -13
- package/scripts/README.md +3 -1
- package/scripts/delivery-presets.mjs +36 -3
- package/scripts/gen-likec4.mjs +149 -16
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +30 -3
- package/scripts/gitops-example.test.mjs +108 -0
- 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 +1 -0
- 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 +216 -8
- package/scripts/local-api.test.mjs +88 -2
- package/scripts/local-discovery.mjs +12 -0
- package/scripts/manifest.mjs +10 -1
- package/scripts/plugin-host.mjs +23 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/run-builtin.mjs +26 -5
- package/scripts/schema.mjs +138 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +12 -6
- package/scripts/warning-policy.test.mjs +40 -3
- 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/SidebarFooter.tsx +20 -4
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +246 -1
- package/src/catalog-validation.ts +104 -2
- package/src/catalog.test.ts +40 -1
- 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.tsx +1 -1
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +21 -0
- package/src/enrich.test.ts +127 -3
- package/src/enrich.ts +104 -6
- 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 +53 -0
- package/src/flow/TraceTrial.tsx +419 -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 +24 -0
- package/src/landing/LandingPage.tsx +15 -11
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- 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/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 +96 -2
- 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/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 +9 -0
- package/src/lib/warnings.ts +10 -10
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +173 -9
- package/src/pages/AggregatePage.tsx +57 -7
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +2 -0
- 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 +58 -2
- package/src/pages/Settings.tsx +28 -2
- package/src/pages/settings/AboutSettings.tsx +8 -1
- 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/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -21,25 +21,31 @@ const ENVIRONMENT = new Environment({
|
|
|
21
21
|
const COMPILED = new Map();
|
|
22
22
|
|
|
23
23
|
const RULES = [
|
|
24
|
-
rule("openapi.missing-operation-id", /\
|
|
24
|
+
rule("openapi.missing-operation-id", /\bno operationId\b/i, "warning", "Add stable operationId values to the OpenAPI operations."),
|
|
25
25
|
rule("schema.duplicate-declaration", /\bduplicate declaration\b/i, "warning", "Remove or reconcile duplicate schema declarations; Portolan currently uses the first."),
|
|
26
26
|
rule("analysis.typed-fallback", /\btyped call graph unavailable\b/i, "warning", "Run an extractor build with typed analysis support or inspect calls found by the syntax fallback."),
|
|
27
27
|
rule("catalog.unresolved-call", /\bcalls .+ which nothing in this catalog resolves\b/i, "warning", "Add or correct the provider contract, then regenerate to resolve the call."),
|
|
28
|
-
rule("catalog.unmapped-proto-peer", /\bmanifest names no peer for that package\b/i, "warning", "Map the protobuf package under peers, or declare it under externals."),
|
|
28
|
+
rule("catalog.unmapped-proto-peer", /\bmanifest names no peer for (?:that package|it)\b/i, "warning", "Map the protobuf package under peers, or declare it under externals."),
|
|
29
29
|
rule("flow.unresolved-step", /\bstep .+ is unresolved\b/i, "warning", "Declare the referenced endpoint, message, or store so this flow step can be joined."),
|
|
30
|
-
rule("flow.unknown-port", /\bis neither a domain port nor a use case\b/i, "warning", "Model this dependency as a domain port or use case, or accept that its calls stay outside the flow."),
|
|
30
|
+
rule("flow.unknown-port", /\bis neither a domain port(?:,| nor) a use case\b/i, "warning", "Model this dependency as a domain port or use case, or accept that its calls stay outside the flow."),
|
|
31
31
|
rule("flow.unreached-event", /\bno flow reaches this event\b/i, "warning", "Connect the event to its publisher flow or remove the stale event declaration."),
|
|
32
32
|
rule("flow.unknown-event", /\breacts to the message named .+ which no event .+ declares is called\b/i, "warning", "Declare the event name used by the handler or correct the handler mapping."),
|
|
33
33
|
rule("django.invalid-aggregate-root", /\baggregates names .+ and no model there is called that\b/i, "warning", "Choose an existing concrete model for the application's aggregates option."),
|
|
34
|
-
rule("django.ambiguous-aggregate-root", /\bmodels to choose from: name the root in the aggregates option\b/i, "warning", "Choose the aggregate root explicitly in the django-domain aggregates option."),
|
|
35
34
|
rule("django.unknown-http-verb", /\bmounted as an HTTP view, but no HTTP verb is declared\b/i, "warning", "Declare the accepted HTTP methods on the view or route."),
|
|
36
35
|
rule("river.missing-worker", /\bno registered Worker\b/i, "warning", "Register the River worker in this component or remove the unmatched insert."),
|
|
37
|
-
rule("watermill.unresolved-topic", /\bWatermill
|
|
36
|
+
rule("watermill.unresolved-topic", /\bWatermill .+\btopic (?:generator could not be resolved|unresolved)\b/i, "warning", "Use a literal, constant, or configuration default for the Watermill topic."),
|
|
38
37
|
rule("messaging.unresolved-subject", /\bsubject of .+ could not be resolved\b/i, "warning", "Use a literal, constant, configuration default, or visible caller argument for the subject."),
|
|
38
|
+
rule("messaging.unresolved-queue", /\bqueue of .+ could not be resolved\b/i, "warning", "Use a literal, constant, configuration default, constructor argument, or visible caller argument for the queue."),
|
|
39
|
+
rule("terraform.unresolved-name", /\b(?:could not be resolved to a literal, a variable default, a local or a module argument|is named with \w+_prefix, so its name is decided at apply time|sets no \w+, so its name is decided at apply time)\b/i, "warning", "Name the resource with a literal, a variable default, a local or a module argument so the catalog can address it."),
|
|
40
|
+
rule("terraform.name-at-apply-time", /\bis `[^`]*`, with .+ decided at apply time\b/i, "info", "The literal part of the name is listed; the rest is filled when Terraform applies."),
|
|
41
|
+
rule("terraform.unresolved-reference", /\bcould not be followed to (?:a queue, a table or a stream|an? \w+(?: \w+)*) declared here\b/i, "warning", "Reference the resource by its Terraform address, or declare it in this module."),
|
|
42
|
+
rule("terraform.module-skipped", /\bmodule ".+" (?:comes from|calls) .+ and is not read\b/i, "info", "Vendor the module under the input root to have its resources read."),
|
|
43
|
+
rule("terraform.not-read", /\bis not read: .+ is not part of this reader yet\b/i, "info", "This AWS product is not modelled yet; the resource is listed so the gap is visible."),
|
|
39
44
|
rule("store.external-migrations", /\bmigrations are applied from .+ whose schema is not in this tree\b/i, "warning", "Vendor or expose the external migrations so their tables can be included in the store."),
|
|
40
45
|
rule("store.missing-foreign-table", /\bcolumn .+ references .+ which no migration here creates\b/i, "warning", "Include the referenced table migration or correct the foreign-key target."),
|
|
41
46
|
rule("schema.unresolved-type", /\bis not declared in the protos read here\b/i, "warning", "Include the imported protobuf declaration or map the type to an external schema."),
|
|
42
|
-
rule("source.offline-cache", /\bnot fetched \(offline\)
|
|
47
|
+
rule("source.offline-cache", /\bnot (?:fetched|read) \(offline\)/i, "info", "Regenerate with network access when the vendored copy must be refreshed."),
|
|
48
|
+
rule("source.unreachable", /\bnot (?:fetched|read) \((?!offline\))/i, "warning", "The far end could not be reached and the committed copy was used; check the server and the credential, then regenerate."),
|
|
43
49
|
rule("source.unpinned", /\bnot pinned\b/i, "warning", "Pin the source to an immutable commit or schema version."),
|
|
44
50
|
rule("source.parse-failed", /\bcould not (?:be read|parse|be parsed|be encoded)\b/i, "error", "Open the referenced source and fix the parse or read error."),
|
|
45
51
|
rule("extraction.no-match", /\b(?:no .+ (?:was|were) found|no .+ matched|declares no |no models in this application)\b/i, "info", "Confirm this capability is absent, or point the extractor at the source that declares it."),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { diagnoseWarnings, warningPolicyProblems } from "./warning-policy.mjs";
|
|
2
|
+
import { classifyWarning, diagnoseWarnings, warningPolicyProblems } from "./warning-policy.mjs";
|
|
3
3
|
|
|
4
4
|
describe("warning CEL policies", () => {
|
|
5
5
|
const warnings = [
|
|
6
|
-
"api:
|
|
7
|
-
"api:
|
|
6
|
+
"aviacore-api: no operationId on 53 of 53 operations; listed by verb and path: GET /a, POST /b and 51 more",
|
|
7
|
+
"aviasupp-api: no operationId on 2 of 9 operations; listed by verb and path: POST /book, POST /cancel",
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
it("evaluates typed CEL over the diagnostic and its repetition count", () => {
|
|
@@ -54,3 +54,40 @@ describe("warning CEL policies", () => {
|
|
|
54
54
|
]);
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
describe("warning classification", () => {
|
|
59
|
+
// Each message is worded the way the plugin that emits it words it today
|
|
60
|
+
// (grep the plugin for the phrase before editing a rule), so a policy that
|
|
61
|
+
// names the rule keeps matching during generation.
|
|
62
|
+
const emitted = [
|
|
63
|
+
["openapi", "aviacore-api: no operationId on 53 of 53 operations; listed by verb and path: GET /a, POST /b and 51 more", "openapi.missing-operation-id"],
|
|
64
|
+
["go-domain", "calls ledger.v1 and the manifest names no peer for that package; add it under `peers` to say which service answers, or under `externals` when the far end is outside the estate, until then the calls are unresolved", "catalog.unmapped-proto-peer"],
|
|
65
|
+
["java-domain", "calls ledger.v1 and the manifest names no peer for it; add it under `peers` to say which service answers, or under `externals` when the far end is outside the estate, until then the calls are unresolved", "catalog.unmapped-proto-peer"],
|
|
66
|
+
["go-domain", "internal/app: port `clock Clock` is neither a domain port nor a use case; its calls are left out of the flow", "flow.unknown-port"],
|
|
67
|
+
["rust-domain", "src/app.rs: port `clock: Clock` is neither a domain port, a use case nor a client; its calls are left out of the flow", "flow.unknown-port"],
|
|
68
|
+
["watermill", "internal/bus/router.go:12:3: Watermill handler orders is registered on topic `cfg.Topic`, which this reader cannot resolve to a literal, a constant, a config default or a caller's argument; the handler is kept with its topic unresolved", "watermill.unresolved-topic"],
|
|
69
|
+
["watermill", "internal/bus/cqrs.go:40:5: Watermill CQRS handler topic generator could not be resolved", "watermill.unresolved-topic"],
|
|
70
|
+
["go-nats", "internal/pub.go:8:2: subject of Publish could not be resolved to a literal, a constant, a config default or a caller's argument", "messaging.unresolved-subject"],
|
|
71
|
+
["go-sqs", "internal/relay.go:15:2: queue of SendMessage could not be resolved to a literal, a constant, a config default, a constructor's argument or a caller's argument", "messaging.unresolved-queue"],
|
|
72
|
+
["terraform", "deploy/queues.tf:12: name of aws_sqs_queue.opaque could not be resolved to a literal, a variable default, a local or a module argument", "terraform.unresolved-name"],
|
|
73
|
+
["terraform", "deploy/queues.tf:40: aws_sqs_queue.generated is named with name_prefix, so its name is decided at apply time and is not read", "terraform.unresolved-name"],
|
|
74
|
+
["terraform", "deploy/lambda.tf:52: aws_lambda_function.unnamed sets no function_name, so its name is decided at apply time and is not read", "terraform.unresolved-name"],
|
|
75
|
+
["terraform", "deploy/data.tf:80: name of aws_sqs_queue.per_account is `fulfillment-events-{account_id}`, with {account_id} decided at apply time", "terraform.name-at-apply-time"],
|
|
76
|
+
["terraform", "deploy/lambda.tf:30: event_source_arn of aws_lambda_event_source_mapping.in could not be followed to a queue, a table or a stream declared here", "terraform.unresolved-reference"],
|
|
77
|
+
["terraform", "deploy/lambda.tf:30: function_name of aws_lambda_event_source_mapping.in could not be followed to an aws_lambda_function declared here", "terraform.unresolved-reference"],
|
|
78
|
+
["terraform", "deploy/main.tf:36: module \"vpc\" comes from terraform-aws-modules/vpc/aws, which is not in this tree, and is not read", "terraform.module-skipped"],
|
|
79
|
+
["terraform", "deploy/data.tf:69: aws_kinesis_stream.clicks is not read: Kinesis is not part of this reader yet", "terraform.not-read"],
|
|
80
|
+
["git", "github.com/acme/ledger: not fetched (offline); the copy committed in this repository is used unchanged", "source.offline-cache"],
|
|
81
|
+
["markdown", "flow.checkout step \"pay\" is unresolved: POST /pay", "flow.unresolved-step"],
|
|
82
|
+
["rust-domain", "Confirm: src/policy.rs: Confirm.handle reacts to the message named \"payment.authorized\", which no event this repository declares is called; the step is unresolved", "flow.unknown-event"],
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
it.each(emitted)("classifies what %s emits to a stable rule", (plugin, message, rule) => {
|
|
86
|
+
expect(classifyWarning(plugin, message).rule).toBe(rule);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("no longer carries a rule for a warning no plugin emits", () => {
|
|
90
|
+
const message = "billing/records: no model called Records, and 2 models to choose from: name the root in the aggregates option";
|
|
91
|
+
expect(classifyWarning("django-domain", message).rule).toMatch(/^plugin\.django-domain\.other-/);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -10,6 +10,9 @@ describe("crumbsFor", () => {
|
|
|
10
10
|
expect(crumbsFor("/language")).toEqual([
|
|
11
11
|
{ label: "language", to: "/language" },
|
|
12
12
|
]);
|
|
13
|
+
expect(crumbsFor("/plugins")).toEqual([
|
|
14
|
+
{ label: "plugins", to: "/plugins" },
|
|
15
|
+
]);
|
|
13
16
|
expect(crumbsFor("/problems")).toEqual([
|
|
14
17
|
{ label: "problems", to: "/problems" },
|
|
15
18
|
]);
|
package/src/app/Breadcrumbs.tsx
CHANGED
|
@@ -85,6 +85,9 @@ export function crumbsFor(pathname: string): Crumb[] {
|
|
|
85
85
|
if (parts[0] === "problems")
|
|
86
86
|
return [{ label: "problems", to: paths.problems() }];
|
|
87
87
|
|
|
88
|
+
if (parts[0] === "plugins")
|
|
89
|
+
return [{ label: "plugins", to: paths.plugins() }];
|
|
90
|
+
|
|
88
91
|
if (parts[0] === "registry") {
|
|
89
92
|
const crumbs: Crumb[] = [{ label: "registry", to: paths.registry() }];
|
|
90
93
|
const slug = parts[1];
|
package/src/app/CatalogApp.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import { FlowDetail } from "../pages/FlowDetail";
|
|
|
18
18
|
import { FlowIndex } from "../pages/FlowIndex";
|
|
19
19
|
import { AdrIndex } from "../pages/AdrIndex";
|
|
20
20
|
import { Language } from "../pages/Language";
|
|
21
|
+
import { PluginIndex } from "../pages/PluginIndex";
|
|
21
22
|
import { AdrDetail } from "../pages/AdrDetail";
|
|
22
23
|
import { Overview } from "../pages/Overview";
|
|
23
24
|
import { ContextMap } from "../pages/ContextMap";
|
|
@@ -119,6 +120,7 @@ function AppRoutes({
|
|
|
119
120
|
/>
|
|
120
121
|
<Route path="/adrs" element={<AdrIndex />} />
|
|
121
122
|
<Route path="/language" element={<Language />} />
|
|
123
|
+
<Route path="/plugins" element={<PluginIndex />} />
|
|
122
124
|
<Route path="/adrs/:adr" element={<AdrDetail />} />
|
|
123
125
|
<Route path="/problems" element={<Problems />} />
|
|
124
126
|
<Route path="/settings/*" element={<Settings />} />
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
1
|
import { NavLink } from "react-router";
|
|
3
2
|
import {
|
|
4
3
|
Check,
|
|
5
4
|
PanelLeftOpen,
|
|
5
|
+
Puzzle,
|
|
6
6
|
Settings2,
|
|
7
7
|
TriangleAlert,
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
|
|
10
10
|
import { allModules } from "../catalog";
|
|
11
|
+
import { catalog } from "../data";
|
|
11
12
|
import { KindIcon } from "../components/kind";
|
|
12
13
|
import { CompassRose } from "../components/logo";
|
|
13
|
-
import {
|
|
14
|
-
import { allProblems } from "../lib/all-problems";
|
|
14
|
+
import { useProblems } from "../lib/use-problems";
|
|
15
15
|
import type { Kind } from "../lib/kinds";
|
|
16
16
|
import { paths } from "../routes";
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@ export function SidebarBottomGroup() {
|
|
|
28
28
|
// opens. The badge is red while anything on it is an error and amber when
|
|
29
29
|
// only the schema disagrees with itself - a page of warnings is not a clean
|
|
30
30
|
// estate, and a green tick over it would be the one lie the row can tell.
|
|
31
|
-
const found =
|
|
31
|
+
const found = useProblems();
|
|
32
32
|
const errors = found.filter((p) => p.severity === "error").length;
|
|
33
33
|
const colour =
|
|
34
34
|
errors > 0 ? "var(--status-unresolved)" : "var(--status-declared)";
|
|
@@ -76,6 +76,22 @@ export function SidebarBottomGroup() {
|
|
|
76
76
|
</span>
|
|
77
77
|
)}
|
|
78
78
|
</NavLink>
|
|
79
|
+
<NavLink
|
|
80
|
+
to={paths.plugins()}
|
|
81
|
+
data-nav-item
|
|
82
|
+
title="Plugins — what Portolan can read, and what it makes"
|
|
83
|
+
style={({ isActive }) => ({
|
|
84
|
+
paddingLeft: indent(0),
|
|
85
|
+
background: isActive ? "var(--surface-2)" : undefined,
|
|
86
|
+
borderLeftWidth: 2,
|
|
87
|
+
borderLeftStyle: "solid",
|
|
88
|
+
borderLeftColor: isActive ? "var(--accent)" : "transparent",
|
|
89
|
+
})}
|
|
90
|
+
className="tree-row flex items-center gap-1.5 py-[3px] pr-2 text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
|
|
91
|
+
>
|
|
92
|
+
<Puzzle size={14} aria-hidden className="block shrink-0" />
|
|
93
|
+
<span className="truncate">Plugins</span>
|
|
94
|
+
</NavLink>
|
|
79
95
|
<NavLink
|
|
80
96
|
to={paths.settings()}
|
|
81
97
|
data-nav-item
|
package/src/catalog-index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
BoundedContext,
|
|
7
7
|
Catalog,
|
|
8
8
|
Column,
|
|
9
|
+
Deployment,
|
|
9
10
|
Enum,
|
|
10
11
|
Event,
|
|
11
12
|
External,
|
|
@@ -21,12 +22,14 @@ import type {
|
|
|
21
22
|
View,
|
|
22
23
|
} from "./catalog-model.ts";
|
|
23
24
|
import {
|
|
25
|
+
allDeployments,
|
|
24
26
|
allExternals,
|
|
25
27
|
allModules,
|
|
26
28
|
allStores,
|
|
27
29
|
allTerms,
|
|
28
30
|
aggregateBlocks,
|
|
29
31
|
columnId,
|
|
32
|
+
deploys,
|
|
30
33
|
enumsOf,
|
|
31
34
|
storeViews,
|
|
32
35
|
viewReads,
|
|
@@ -140,6 +143,14 @@ export interface CatalogIndex {
|
|
|
140
143
|
lineageInto: Map<string, string[]>;
|
|
141
144
|
/** service id -> stores it owns, in catalog order */
|
|
142
145
|
storesOwnedBy: Map<string, Store[]>;
|
|
146
|
+
/**
|
|
147
|
+
* service id -> where it runs, in snapshot order.
|
|
148
|
+
*
|
|
149
|
+
* Joined here rather than written by the fetcher, which reads a control
|
|
150
|
+
* plane and knows no service: an Application is this service's when it
|
|
151
|
+
* deploys from the service's repository, inside the service's directory.
|
|
152
|
+
*/
|
|
153
|
+
deploymentsByService: Map<string, Deployment[]>;
|
|
143
154
|
/** aggregate id -> tables naming it in `persists`, in catalog order */
|
|
144
155
|
tablesByAggregate: Map<string, Table[]>;
|
|
145
156
|
/** aggregate id -> views naming it in `persists`, in catalog order */
|
|
@@ -323,6 +334,19 @@ export function buildIndex(catalog: Catalog): CatalogIndex {
|
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
336
|
|
|
337
|
+
// Deployments come after the services because the join runs over them: a
|
|
338
|
+
// row is placed on every service whose repository and directory it deploys
|
|
339
|
+
// from, and on none when nobody in the estate claims that repository.
|
|
340
|
+
const deploymentsByService = new Map<string, Deployment[]>();
|
|
341
|
+
for (const deployment of allDeployments(catalog)) {
|
|
342
|
+
for (const service of serviceById.values()) {
|
|
343
|
+
if (!deploys(deployment, service)) continue;
|
|
344
|
+
const placed = deploymentsByService.get(service.id) ?? [];
|
|
345
|
+
placed.push(deployment);
|
|
346
|
+
deploymentsByService.set(service.id, placed);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
326
350
|
// Stores come after the domain tree because they point into it: a table says
|
|
327
351
|
// which aggregate it persists, and a column which block it maps to, so both
|
|
328
352
|
// are resolved against maps that are already full.
|
|
@@ -467,6 +491,7 @@ export function buildIndex(catalog: Catalog): CatalogIndex {
|
|
|
467
491
|
lineageFrom,
|
|
468
492
|
lineageInto,
|
|
469
493
|
storesOwnedBy,
|
|
494
|
+
deploymentsByService,
|
|
470
495
|
moduleById,
|
|
471
496
|
moduleBySlug,
|
|
472
497
|
interfacesByModule,
|
package/src/catalog-model.ts
CHANGED
|
@@ -44,6 +44,13 @@ export interface Catalog {
|
|
|
44
44
|
* to pin and renders as it did before there was anything to pin.
|
|
45
45
|
*/
|
|
46
46
|
repos?: RepoPin[];
|
|
47
|
+
/**
|
|
48
|
+
* Where the estate's services run, read from what deploys them. Optional
|
|
49
|
+
* in the file and never optional downstream, exactly like `repos`: an
|
|
50
|
+
* estate nobody has pointed at a deployer renders as it did before there
|
|
51
|
+
* was anything to place.
|
|
52
|
+
*/
|
|
53
|
+
deployments?: Deployment[];
|
|
47
54
|
/**
|
|
48
55
|
* The systems outside the estate that a service calls on a contract: a
|
|
49
56
|
* payment provider, a tax API, a carrier. Nobody here builds one, so it has
|
|
@@ -173,6 +180,24 @@ export interface Service {
|
|
|
173
180
|
* same as a service that cannot be built.
|
|
174
181
|
*/
|
|
175
182
|
commands?: Command[];
|
|
183
|
+
/**
|
|
184
|
+
* The names this service answers on, read from what deploys it: a
|
|
185
|
+
* Kubernetes Service's name in its short, namespaced, `svc` and fully
|
|
186
|
+
* qualified forms, and the hosts of the Ingress or HTTPRoute in front of
|
|
187
|
+
* it. Written so that a call another service is configured to make to
|
|
188
|
+
* `pricing.shop.svc` can find the service that answers. Absent when
|
|
189
|
+
* nothing in the tree says where the service is reachable.
|
|
190
|
+
*/
|
|
191
|
+
hosts?: string[];
|
|
192
|
+
/**
|
|
193
|
+
* The in-cluster names this service's workload is configured to reach,
|
|
194
|
+
* read out of its environment and config maps and reduced to the host
|
|
195
|
+
* alone. A value is never kept: not the variable it came from, not the
|
|
196
|
+
* scheme, port, path or credentials around the name. Only a name the
|
|
197
|
+
* cluster resolves qualifies, so a password in an environment variable is
|
|
198
|
+
* not something this list can hold by shape.
|
|
199
|
+
*/
|
|
200
|
+
dials?: string[];
|
|
176
201
|
}
|
|
177
202
|
|
|
178
203
|
/**
|
|
@@ -377,7 +402,7 @@ export interface HTTPDestination {
|
|
|
377
402
|
join?: { expression: string; source: string };
|
|
378
403
|
/** Runtime URL modifiers after the proven join; not evaluated statically. */
|
|
379
404
|
transforms?: { expression: string; source: string }[];
|
|
380
|
-
resolution?: { basis: "full-path" | "exact-route" | "unique-suffix"; provider: string; route: string };
|
|
405
|
+
resolution?: { basis: "full-path" | "exact-route" | "unique-suffix" | "kubernetes-host"; provider: string; route: string };
|
|
381
406
|
}
|
|
382
407
|
export interface HTTPBaseURL {
|
|
383
408
|
expression: string;
|
|
@@ -471,6 +496,104 @@ export interface RepoPin {
|
|
|
471
496
|
/** The commit the copy was made of. Full sha: it is not resolved locally, so there is nothing to expand it against. */
|
|
472
497
|
commit: string;
|
|
473
498
|
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* One place a service runs: an Argo CD Application, as the deployer listed
|
|
502
|
+
* it, reduced to what a deploy changes.
|
|
503
|
+
*
|
|
504
|
+
* A list on the catalog rather than a field on `Service`, for two reasons.
|
|
505
|
+
* The fetcher that writes it reads a control plane, not a service's tree, so
|
|
506
|
+
* it does not know which service an Application is; the join is made where
|
|
507
|
+
* the whole estate is known, by the repository and path the Application
|
|
508
|
+
* deploys from against the ones a service says it lives at. And one service
|
|
509
|
+
* stands in several places - staging, production, a second region - and
|
|
510
|
+
* each is its own row.
|
|
511
|
+
*
|
|
512
|
+
* Nothing here moves without a deploy. Health, sync state and the time of
|
|
513
|
+
* the last operation move on their own, and a page regenerated from the
|
|
514
|
+
* tree cannot follow them; they are not kept.
|
|
515
|
+
*/
|
|
516
|
+
export interface Deployment {
|
|
517
|
+
/** `<argocd namespace>/<application name>`: unique per control plane. */
|
|
518
|
+
id: string;
|
|
519
|
+
/** The Application's name, as `argocd app get` takes it. */
|
|
520
|
+
name: string;
|
|
521
|
+
/** The Argo CD project it belongs to. */
|
|
522
|
+
project: string;
|
|
523
|
+
/**
|
|
524
|
+
* Where a reader would say it runs: the application's environment label,
|
|
525
|
+
* else the cluster it deploys to. What the page groups by.
|
|
526
|
+
*/
|
|
527
|
+
environment: string;
|
|
528
|
+
/** The destination cluster, by its Argo CD name; `in-cluster` for the one Argo CD runs in. */
|
|
529
|
+
cluster: string;
|
|
530
|
+
/** The destination namespace. */
|
|
531
|
+
namespace: string;
|
|
532
|
+
/** The repository it deploys from, spelled the way `Service.repo` spells it. Empty for a chart from a registry. */
|
|
533
|
+
repo: string;
|
|
534
|
+
/** The directory in that repository the manifests are read from, as a reader would type it. Empty for a chart. */
|
|
535
|
+
path: string;
|
|
536
|
+
/** The Helm chart name, when the source is a chart registry rather than a repository. */
|
|
537
|
+
chart?: string;
|
|
538
|
+
/** What the Application tracks: a branch, a tag, a chart version. */
|
|
539
|
+
targetRevision: string;
|
|
540
|
+
/** What is deployed now: the commit, or chart version, the last sync resolved `targetRevision` to. */
|
|
541
|
+
revision: string;
|
|
542
|
+
/** `helm`, `kustomize`, `directory` or `plugin`, as Argo CD says it; empty until it has said. */
|
|
543
|
+
tool: string;
|
|
544
|
+
/** The Application in the Argo CD UI. */
|
|
545
|
+
url: string;
|
|
546
|
+
/**
|
|
547
|
+
* The service it deploys, by id, when the Application's labels say so:
|
|
548
|
+
* `app.kubernetes.io/part-of` and `app.kubernetes.io/name`, the same
|
|
549
|
+
* labels a workload carries. A GitOps repository's Application points at
|
|
550
|
+
* an overlay, not at the service's directory, so the path alone cannot
|
|
551
|
+
* place it; the labels can, and an ApplicationSet stamps them on every
|
|
552
|
+
* Application it makes. Absent when the labels are, and the path decides.
|
|
553
|
+
*/
|
|
554
|
+
service?: string;
|
|
555
|
+
/** The container images the deployed resources run, as the deployer summarised them, sorted. */
|
|
556
|
+
images?: string[];
|
|
557
|
+
/**
|
|
558
|
+
* Who said so. `manifest` is a row an extractor read out of a GitOps
|
|
559
|
+
* tree: what should run. `api` is a row a fetcher read off the deployer:
|
|
560
|
+
* what does. `both` is one the merge folded from the two, which is the
|
|
561
|
+
* ordinary case for an estate that keeps both, and the only one that can
|
|
562
|
+
* carry drift. Absent reads as `api`, which is what every row was before
|
|
563
|
+
* there were trees to read.
|
|
564
|
+
*/
|
|
565
|
+
basis?: DeploymentBasis;
|
|
566
|
+
/**
|
|
567
|
+
* What the tree says where it and the deployer disagree, set by the merge
|
|
568
|
+
* when both spoke (portolan.0013). A field is present only when the two
|
|
569
|
+
* differ; a row with no drift has no field at all.
|
|
570
|
+
*/
|
|
571
|
+
drift?: DeploymentDrift;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export type DeploymentBasis = "manifest" | "api" | "both";
|
|
575
|
+
|
|
576
|
+
export const DEPLOYMENT_BASES: readonly DeploymentBasis[] = [
|
|
577
|
+
"manifest",
|
|
578
|
+
"api",
|
|
579
|
+
"both",
|
|
580
|
+
] as const;
|
|
581
|
+
|
|
582
|
+
/** The GitOps tree's word on the fields where the deployer says otherwise. */
|
|
583
|
+
export interface DeploymentDrift {
|
|
584
|
+
project?: string;
|
|
585
|
+
cluster?: string;
|
|
586
|
+
namespace?: string;
|
|
587
|
+
path?: string;
|
|
588
|
+
targetRevision?: string;
|
|
589
|
+
/** The images the overlay pins, when they are not the ones running. */
|
|
590
|
+
images?: string[];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The basis a row carries, `api` when it carries none. */
|
|
594
|
+
export function deploymentBasis(deployment: Deployment): DeploymentBasis {
|
|
595
|
+
return deployment.basis ?? "api";
|
|
596
|
+
}
|
|
474
597
|
export interface Aggregate {
|
|
475
598
|
id: string;
|
|
476
599
|
slug: string;
|
|
@@ -537,6 +660,15 @@ export interface Operation {
|
|
|
537
660
|
* estate can only reach from inside, which is sometimes exactly the point.
|
|
538
661
|
*/
|
|
539
662
|
exposedBy?: string[];
|
|
663
|
+
/**
|
|
664
|
+
* What the caller hands in: the command's or query's own shape, as the
|
|
665
|
+
* message class declares it - the fields of `CreateCourseCommand`. Absent
|
|
666
|
+
* when the extractor does not read messages; empty is a message that
|
|
667
|
+
* carries nothing, `FindCoursesCounterQuery`.
|
|
668
|
+
*/
|
|
669
|
+
fields?: Field[];
|
|
670
|
+
/** Where the handler is, `path:line`, for the reader who wants the code. */
|
|
671
|
+
source?: string;
|
|
540
672
|
}
|
|
541
673
|
|
|
542
674
|
/**
|
|
@@ -687,7 +819,31 @@ export interface Field {
|
|
|
687
819
|
ref?: string;
|
|
688
820
|
/** Protobuf field number; absent for sources whose wire has no field numbers. */
|
|
689
821
|
number?: number;
|
|
822
|
+
/**
|
|
823
|
+
* The source says the field must be sent: a Protovalidate `required`, a
|
|
824
|
+
* name in a JSON Schema `required` list. Absent means the source does not
|
|
825
|
+
* say, which in proto3 and OpenAPI alike means it may be left out.
|
|
826
|
+
*/
|
|
827
|
+
required?: boolean;
|
|
828
|
+
/** What the source says a value must satisfy, in the order it said it. */
|
|
829
|
+
rules?: FieldRule[];
|
|
690
830
|
} // ref -> defs key
|
|
831
|
+
/**
|
|
832
|
+
* One constraint on a field's value, in the catalog's own vocabulary so a
|
|
833
|
+
* Protovalidate `min_len` and a JSON Schema `minLength` are one rule:
|
|
834
|
+
* `min_len`, `max_len`, `len`, `pattern`, `prefix`, `suffix`, `contains`,
|
|
835
|
+
* `not_contains`, `format`, `gt`, `gte`, `lt`, `lte`, `const`, `in`,
|
|
836
|
+
* `not_in`, `multiple_of`, `min_items`, `max_items`, `unique`, `min_pairs`,
|
|
837
|
+
* `max_pairs`, `defined_only`, `lt_now`, `gt_now`, `cel`. A rule on what a
|
|
838
|
+
* list holds is prefixed `items.`; on a map's keys or values, `keys.` or
|
|
839
|
+
* `values.`. A custom option the catalog has no word for keeps the name the
|
|
840
|
+
* source gave it, `(acme.pii)`, so it is shown rather than lost.
|
|
841
|
+
*/
|
|
842
|
+
export interface FieldRule {
|
|
843
|
+
name: string;
|
|
844
|
+
/** The bound as the source wrote it, text so a 64-bit number survives; absent for a bare flag such as `unique`. */
|
|
845
|
+
value?: string;
|
|
846
|
+
}
|
|
691
847
|
export interface TypeDef {
|
|
692
848
|
fields: Field[];
|
|
693
849
|
}
|
|
@@ -710,6 +866,7 @@ export type StoreKind =
|
|
|
710
866
|
| "mongodb"
|
|
711
867
|
| "clickhouse"
|
|
712
868
|
| "s3"
|
|
869
|
+
| "dynamodb"
|
|
713
870
|
| "other";
|
|
714
871
|
|
|
715
872
|
export const STORE_KINDS: readonly StoreKind[] = [
|
|
@@ -720,6 +877,7 @@ export const STORE_KINDS: readonly StoreKind[] = [
|
|
|
720
877
|
"mongodb",
|
|
721
878
|
"clickhouse",
|
|
722
879
|
"s3",
|
|
880
|
+
"dynamodb",
|
|
723
881
|
"other",
|
|
724
882
|
] as const;
|
|
725
883
|
|
|
@@ -918,6 +1076,47 @@ export interface Flow {
|
|
|
918
1076
|
owner: string;
|
|
919
1077
|
participants: Participant[]; // order is significant - it is the lane order
|
|
920
1078
|
steps: FlowNode[];
|
|
1079
|
+
/**
|
|
1080
|
+
* Recordings of this flow running: one per trace a verifier kept, each
|
|
1081
|
+
* naming the steps it showed with what the spans said about them. Examples
|
|
1082
|
+
* of data, not evidence - the evidence is the status on the step.
|
|
1083
|
+
*/
|
|
1084
|
+
examples?: FlowExample[];
|
|
1085
|
+
}
|
|
1086
|
+
/** One recorded run of a flow, read from one trace. */
|
|
1087
|
+
export interface FlowExample {
|
|
1088
|
+
/** "<recording>#<trace id>": unique within the flow, stable across runs. */
|
|
1089
|
+
id: string;
|
|
1090
|
+
/**
|
|
1091
|
+
* The file the trace was read from, relative to the verify step's input
|
|
1092
|
+
* root - where an uploaded recording is kept.
|
|
1093
|
+
*/
|
|
1094
|
+
recording: string;
|
|
1095
|
+
traceId: string;
|
|
1096
|
+
/** When the root span started, RFC 3339 UTC; absent when the recording has no clock. */
|
|
1097
|
+
recordedAt?: string;
|
|
1098
|
+
/** Root start to the last end the trace shows. */
|
|
1099
|
+
durationMs: number;
|
|
1100
|
+
/** The steps this trace showed, in the order it showed them. */
|
|
1101
|
+
steps: ExampleStep[];
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* What one span said about one step. Only an allowlist of attributes is
|
|
1105
|
+
* carried: names, verbs, routes, status codes - never a query text, a header
|
|
1106
|
+
* or a path with an id in it.
|
|
1107
|
+
*/
|
|
1108
|
+
export interface ExampleStep {
|
|
1109
|
+
step: string;
|
|
1110
|
+
label?: string;
|
|
1111
|
+
durationMs: number;
|
|
1112
|
+
attributes?: Record<string, string>;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* How many recordings showed a step. On a declared step it accompanies the
|
|
1116
|
+
* raised status; on a step no source declares it is why the step is there.
|
|
1117
|
+
*/
|
|
1118
|
+
export interface StepSeen {
|
|
1119
|
+
traces: number;
|
|
921
1120
|
}
|
|
922
1121
|
export interface FlowTrigger {
|
|
923
1122
|
kind:
|
|
@@ -974,6 +1173,8 @@ export interface Step {
|
|
|
974
1173
|
handoff?: FlowHandoff;
|
|
975
1174
|
/** Repository call resolved to a concrete store operation after merge. */
|
|
976
1175
|
storeAccess?: FlowStoreAccess;
|
|
1176
|
+
/** Recordings that showed this hop; set by a verifier. */
|
|
1177
|
+
seen?: StepSeen;
|
|
977
1178
|
}
|
|
978
1179
|
export interface HTTPResponse {
|
|
979
1180
|
status?: number;
|
|
@@ -1029,6 +1230,12 @@ export interface AltBranch {
|
|
|
1029
1230
|
steps: FlowNode[];
|
|
1030
1231
|
/** True when the flow stops here instead of continuing past the alt. */
|
|
1031
1232
|
terminal?: boolean;
|
|
1233
|
+
/**
|
|
1234
|
+
* Recordings that went this way, on a frame a verifier wrote where the
|
|
1235
|
+
* recordings parted. A branch with no steps and a count is the recordings
|
|
1236
|
+
* that went no further.
|
|
1237
|
+
*/
|
|
1238
|
+
seen?: StepSeen;
|
|
1032
1239
|
}
|
|
1033
1240
|
export interface Loop {
|
|
1034
1241
|
type: "loop";
|
|
@@ -1272,6 +1479,44 @@ export function allRepos(catalog: Catalog): RepoPin[] {
|
|
|
1272
1479
|
return catalog.repos ?? [];
|
|
1273
1480
|
}
|
|
1274
1481
|
|
|
1482
|
+
/** Every place a service runs. Absent means none, exactly as with repos. */
|
|
1483
|
+
export function allDeployments(catalog: Catalog): Deployment[] {
|
|
1484
|
+
return catalog.deployments ?? [];
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Whether a deployment is of this service. The labels decide when the
|
|
1489
|
+
* Application carries them: `service` is the id, and an Application
|
|
1490
|
+
* labelled for one service is not another's however its path reads. Without
|
|
1491
|
+
* labels, the path: the same repository, and the manifests read from inside
|
|
1492
|
+
* the service's directory - or from anywhere in the repository when the
|
|
1493
|
+
* service is the whole of it. An Application that matches nothing is a fact
|
|
1494
|
+
* for the Problems page rather than for a guess.
|
|
1495
|
+
*/
|
|
1496
|
+
export function deploys(deployment: Deployment, service: Service): boolean {
|
|
1497
|
+
if (deployment.service) return deployment.service === service.id;
|
|
1498
|
+
if (!deployment.repo || deployment.repo !== service.repo) return false;
|
|
1499
|
+
const root = service.path.replace(/^\/+|\/+$/g, "");
|
|
1500
|
+
if (!root) return true;
|
|
1501
|
+
return deployment.path === root || deployment.path.startsWith(`${root}/`);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* The environment a reader would say a deployment stands in: the one the
|
|
1506
|
+
* deployer named, else the cluster, else the one word that says neither was
|
|
1507
|
+
* said. Every picture and filter groups by this, so it is decided once.
|
|
1508
|
+
*/
|
|
1509
|
+
export function environmentOf(deployment: Deployment): string {
|
|
1510
|
+
return deployment.environment || deployment.cluster || "unplaced";
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/** Where this service runs, in catalog order: the snapshot's, which is by id. */
|
|
1514
|
+
export function deploymentsOf(catalog: Catalog, service: Service): Deployment[] {
|
|
1515
|
+
return allDeployments(catalog).filter((deployment) =>
|
|
1516
|
+
deploys(deployment, service),
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1275
1520
|
/** Who to ask about a service, without the caller having to know the field is optional. */
|
|
1276
1521
|
export function ownersOf(service: Service): string[] {
|
|
1277
1522
|
return service.owners ?? [];
|