diff --git a/conformance/README.md b/conformance/README.md index 2a56523..a7e3549 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -163,12 +163,13 @@ later-phase diagnostic before the earlier phases pass. ## Coverage status `parser`, `structural` and `semantic` are covered. Every diagnostic code the -three `spec.md` files declare is exercised by at least one case, with one -exception: +three `spec.md` files declare is exercised by at least one case, with two +exceptions, both `capability`: | Code | Why it has no case | |---|---| | `ERR_UNKNOWN_COMPONENT` | `capability` — resolving a published reference needs the catalog, and no phase a client runs may reach the network | +| `ERR_VERSION_NOT_MONOTONIC` | `capability` — comparing a version against the lineage it extends needs the catalog, and a fixture is one document with no previous release to be greater than | That table is not prose anyone has to remember to update. `task check:conformance` derives it: every `ERR_*` row in a family's own diff --git a/conformance/component/v1/cases.json b/conformance/component/v1/cases.json index 6d45beb..e52f0bb 100644 --- a/conformance/component/v1/cases.json +++ b/conformance/component/v1/cases.json @@ -137,6 +137,41 @@ "phase": "structural", "path": "structural/023-field-from-newer-release" }, + { + "id": "structural-024-container-port-below-range", + "phase": "structural", + "path": "structural/024-container-port-below-range" + }, + { + "id": "structural-025-container-port-above-range", + "phase": "structural", + "path": "structural/025-container-port-above-range" + }, + { + "id": "structural-026-privileged-container-port", + "phase": "structural", + "path": "structural/026-privileged-container-port" + }, + { + "id": "structural-027-public-tcp-endpoint", + "phase": "structural", + "path": "structural/027-public-tcp-endpoint" + }, + { + "id": "structural-028-public-grpc-endpoint", + "phase": "structural", + "path": "structural/028-public-grpc-endpoint" + }, + { + "id": "structural-029-endpoint-name-not-a-slug", + "phase": "structural", + "path": "structural/029-endpoint-name-not-a-slug" + }, + { + "id": "structural-030-two-public-endpoints", + "phase": "structural", + "path": "structural/030-two-public-endpoints" + }, { "id": "semantic-001-floating-tag-image-reference", "phase": "semantic", @@ -146,6 +181,31 @@ "id": "semantic-002-probe-names-unknown-endpoint", "phase": "semantic", "path": "semantic/002-probe-names-unknown-endpoint" + }, + { + "id": "semantic-003-platform-default-names-unknown-endpoint", + "phase": "semantic", + "path": "semantic/003-platform-default-names-unknown-endpoint" + }, + { + "id": "semantic-004-platform-default-names-private-endpoint", + "phase": "semantic", + "path": "semantic/004-platform-default-names-private-endpoint" + }, + { + "id": "semantic-005-no-primary-endpoint-elected", + "phase": "semantic", + "path": "semantic/005-no-primary-endpoint-elected" + }, + { + "id": "semantic-006-primary-endpoint-elected", + "phase": "semantic", + "path": "semantic/006-primary-endpoint-elected" + }, + { + "id": "semantic-007-probe-on-endpointless-workload", + "phase": "semantic", + "path": "semantic/007-probe-on-endpointless-workload" } ] } diff --git a/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/case.yaml b/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/case.yaml new file mode 100644 index 0000000..3855e47 --- /dev/null +++ b/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/case.yaml @@ -0,0 +1,33 @@ +# The platform default derives from "console", and the only endpoint is "api". +# JSON Schema cannot catch it: the endpoint names are mapping keys elsewhere +# in the document. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + api: + containerPort: 8080 + protocol: HTTP + visibility: PUBLIC + health: + readiness: + path: /healthz + endpoint: api + contract: + inputs: + consoleUrl: + schema: + type: STRING + platformDefault: + source: PUBLIC_URL + endpoint: console + ui: + label: Console URL + outputs: {} diff --git a/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/diagnostics.json b/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/diagnostics.json new file mode 100644 index 0000000..068a4ad --- /dev/null +++ b/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_UNKNOWN_ENDPOINT", + "path": "/spec/contract/inputs/consoleUrl/platformDefault/endpoint" + } +] diff --git a/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/metadata.json b/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/metadata.json new file mode 100644 index 0000000..ea2fe00 --- /dev/null +++ b/conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "semantic-003-platform-default-names-unknown-endpoint", + "phase": "semantic", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#inputs", + "summary": "A platform default naming an endpoint the workload does not declare is rejected." +} diff --git a/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/case.yaml b/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/case.yaml new file mode 100644 index 0000000..8dcc154 --- /dev/null +++ b/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/case.yaml @@ -0,0 +1,28 @@ +# PUBLIC_URL derives an externally reachable address, and a PRIVATE endpoint +# has none to give. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + internal: + containerPort: 8080 + protocol: HTTP + visibility: PRIVATE + contract: + inputs: + selfUrl: + schema: + type: STRING + platformDefault: + source: PUBLIC_URL + endpoint: internal + ui: + label: Own URL + outputs: {} diff --git a/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/diagnostics.json b/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/diagnostics.json new file mode 100644 index 0000000..80d36e0 --- /dev/null +++ b/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_ENDPOINT_NOT_PUBLIC", + "path": "/spec/contract/inputs/selfUrl/platformDefault/endpoint" + } +] diff --git a/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/metadata.json b/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/metadata.json new file mode 100644 index 0000000..d2efe6c --- /dev/null +++ b/conformance/component/v1/semantic/004-platform-default-names-private-endpoint/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "semantic-004-platform-default-names-private-endpoint", + "phase": "semantic", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#inputs", + "summary": "A platform default deriving a public address from a PRIVATE endpoint is rejected." +} diff --git a/conformance/component/v1/semantic/005-no-primary-endpoint-elected/case.yaml b/conformance/component/v1/semantic/005-no-primary-endpoint-elected/case.yaml new file mode 100644 index 0000000..48a8a2a --- /dev/null +++ b/conformance/component/v1/semantic/005-no-primary-endpoint-elected/case.yaml @@ -0,0 +1,25 @@ +# Two PUBLIC endpoints, and the probe names neither. Section 5.2 elects no +# primary here and the reference is rejected rather than resolved by sort +# order, which would let a new endpoint silently re-point this probe. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + api: + containerPort: 8080 + protocol: HTTP + visibility: PUBLIC + console: + containerPort: 8081 + protocol: HTTP + visibility: PUBLIC + health: + readiness: + path: /healthz diff --git a/conformance/component/v1/semantic/005-no-primary-endpoint-elected/diagnostics.json b/conformance/component/v1/semantic/005-no-primary-endpoint-elected/diagnostics.json new file mode 100644 index 0000000..5c6aeaf --- /dev/null +++ b/conformance/component/v1/semantic/005-no-primary-endpoint-elected/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_AMBIGUOUS_ENDPOINT", + "path": "/spec/workload/health/readiness/endpoint" + } +] diff --git a/conformance/component/v1/semantic/005-no-primary-endpoint-elected/metadata.json b/conformance/component/v1/semantic/005-no-primary-endpoint-elected/metadata.json new file mode 100644 index 0000000..616e251 --- /dev/null +++ b/conformance/component/v1/semantic/005-no-primary-endpoint-elected/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "semantic-005-no-primary-endpoint-elected", + "phase": "semantic", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "A reference omitting the endpoint is rejected where the workload elects no primary." +} diff --git a/conformance/component/v1/semantic/006-primary-endpoint-elected/case.yaml b/conformance/component/v1/semantic/006-primary-endpoint-elected/case.yaml new file mode 100644 index 0000000..ab2b96a --- /dev/null +++ b/conformance/component/v1/semantic/006-primary-endpoint-elected/case.yaml @@ -0,0 +1,24 @@ +# One PUBLIC endpoint beside a PRIVATE one. The sole PUBLIC endpoint is the +# primary, so the probe naming none resolves to "web". +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + web: + containerPort: 8080 + protocol: HTTP + visibility: PUBLIC + metrics: + containerPort: 9090 + protocol: HTTP + visibility: PRIVATE + health: + readiness: + path: /healthz diff --git a/conformance/component/v1/semantic/006-primary-endpoint-elected/metadata.json b/conformance/component/v1/semantic/006-primary-endpoint-elected/metadata.json new file mode 100644 index 0000000..e26b4ed --- /dev/null +++ b/conformance/component/v1/semantic/006-primary-endpoint-elected/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "semantic-006-primary-endpoint-elected", + "phase": "semantic", + "expected": "pass", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "The sole PUBLIC endpoint is the primary a probe naming none resolves to." +} diff --git a/conformance/component/v1/semantic/007-probe-on-endpointless-workload/case.yaml b/conformance/component/v1/semantic/007-probe-on-endpointless-workload/case.yaml new file mode 100644 index 0000000..f39eff4 --- /dev/null +++ b/conformance/component/v1/semantic/007-probe-on-endpointless-workload/case.yaml @@ -0,0 +1,17 @@ +# Section 5 permits a SERVICE to declare no endpoint. A probe on one polls a +# port that does not exist: the election finds no primary here for the +# opposite reason it finds none among two PUBLIC endpoints, and rejects it +# just the same. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + health: + liveness: + path: /healthz diff --git a/conformance/component/v1/semantic/007-probe-on-endpointless-workload/diagnostics.json b/conformance/component/v1/semantic/007-probe-on-endpointless-workload/diagnostics.json new file mode 100644 index 0000000..eebe861 --- /dev/null +++ b/conformance/component/v1/semantic/007-probe-on-endpointless-workload/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_AMBIGUOUS_ENDPOINT", + "path": "/spec/workload/health/liveness/endpoint" + } +] diff --git a/conformance/component/v1/semantic/007-probe-on-endpointless-workload/metadata.json b/conformance/component/v1/semantic/007-probe-on-endpointless-workload/metadata.json new file mode 100644 index 0000000..322b821 --- /dev/null +++ b/conformance/component/v1/semantic/007-probe-on-endpointless-workload/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "semantic-007-probe-on-endpointless-workload", + "phase": "semantic", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "A probe on a workload declaring no endpoint has no port to poll." +} diff --git a/conformance/component/v1/structural/024-container-port-below-range/case.yaml b/conformance/component/v1/structural/024-container-port-below-range/case.yaml new file mode 100644 index 0000000..5798ff4 --- /dev/null +++ b/conformance/component/v1/structural/024-container-port-below-range/case.yaml @@ -0,0 +1,16 @@ +# Port 0 binds nothing. The bound is a grammar, so the schema carries it. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + web: + containerPort: 0 + protocol: HTTP + visibility: PRIVATE diff --git a/conformance/component/v1/structural/024-container-port-below-range/diagnostics.json b/conformance/component/v1/structural/024-container-port-below-range/diagnostics.json new file mode 100644 index 0000000..2bc7a78 --- /dev/null +++ b/conformance/component/v1/structural/024-container-port-below-range/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/workload/endpoints/web/containerPort" + } +] diff --git a/conformance/component/v1/structural/024-container-port-below-range/metadata.json b/conformance/component/v1/structural/024-container-port-below-range/metadata.json new file mode 100644 index 0000000..7bf5c73 --- /dev/null +++ b/conformance/component/v1/structural/024-container-port-below-range/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-024-container-port-below-range", + "phase": "structural", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "A containerPort below 1 is rejected." +} diff --git a/conformance/component/v1/structural/025-container-port-above-range/case.yaml b/conformance/component/v1/structural/025-container-port-above-range/case.yaml new file mode 100644 index 0000000..eb63685 --- /dev/null +++ b/conformance/component/v1/structural/025-container-port-above-range/case.yaml @@ -0,0 +1,16 @@ +# 65535 is the last port there is; 65536 names one that cannot exist. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + web: + containerPort: 65536 + protocol: HTTP + visibility: PRIVATE diff --git a/conformance/component/v1/structural/025-container-port-above-range/diagnostics.json b/conformance/component/v1/structural/025-container-port-above-range/diagnostics.json new file mode 100644 index 0000000..2bc7a78 --- /dev/null +++ b/conformance/component/v1/structural/025-container-port-above-range/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/workload/endpoints/web/containerPort" + } +] diff --git a/conformance/component/v1/structural/025-container-port-above-range/metadata.json b/conformance/component/v1/structural/025-container-port-above-range/metadata.json new file mode 100644 index 0000000..40bc6db --- /dev/null +++ b/conformance/component/v1/structural/025-container-port-above-range/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-025-container-port-above-range", + "phase": "structural", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "A containerPort above 65535 is rejected." +} diff --git a/conformance/component/v1/structural/026-privileged-container-port/case.yaml b/conformance/component/v1/structural/026-privileged-container-port/case.yaml new file mode 100644 index 0000000..846a651 --- /dev/null +++ b/conformance/component/v1/structural/026-privileged-container-port/case.yaml @@ -0,0 +1,18 @@ +# A port below 1024 is discouraged and not rejected: whether the container can +# bind one depends on a capability the runtime grants, which this document +# cannot see. This case is what keeps that a SHOULD NOT. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + web: + containerPort: 80 + protocol: HTTP + visibility: PRIVATE diff --git a/conformance/component/v1/structural/026-privileged-container-port/metadata.json b/conformance/component/v1/structural/026-privileged-container-port/metadata.json new file mode 100644 index 0000000..6c073f0 --- /dev/null +++ b/conformance/component/v1/structural/026-privileged-container-port/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-026-privileged-container-port", + "phase": "structural", + "expected": "pass", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "A privileged containerPort is discouraged but validates." +} diff --git a/conformance/component/v1/structural/027-public-tcp-endpoint/case.yaml b/conformance/component/v1/structural/027-public-tcp-endpoint/case.yaml new file mode 100644 index 0000000..9526c5e --- /dev/null +++ b/conformance/component/v1/structural/027-public-tcp-endpoint/case.yaml @@ -0,0 +1,22 @@ +# A PUBLIC endpoint is published at a URL by something that speaks the HTTP +# family, and TCP has no URL to publish. Readiness is present so the case +# fails on the protocol rule alone. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: postgres:17.10-alpine + endpoints: + db: + containerPort: 5432 + protocol: TCP + visibility: PUBLIC + health: + readiness: + path: /healthz + endpoint: db diff --git a/conformance/component/v1/structural/027-public-tcp-endpoint/diagnostics.json b/conformance/component/v1/structural/027-public-tcp-endpoint/diagnostics.json new file mode 100644 index 0000000..a531e51 --- /dev/null +++ b/conformance/component/v1/structural/027-public-tcp-endpoint/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/workload/endpoints/db/protocol" + } +] diff --git a/conformance/component/v1/structural/027-public-tcp-endpoint/metadata.json b/conformance/component/v1/structural/027-public-tcp-endpoint/metadata.json new file mode 100644 index 0000000..ee2a2ed --- /dev/null +++ b/conformance/component/v1/structural/027-public-tcp-endpoint/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-027-public-tcp-endpoint", + "phase": "structural", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "A TCP endpoint may not be PUBLIC." +} diff --git a/conformance/component/v1/structural/028-public-grpc-endpoint/case.yaml b/conformance/component/v1/structural/028-public-grpc-endpoint/case.yaml new file mode 100644 index 0000000..b3ce514 --- /dev/null +++ b/conformance/component/v1/structural/028-public-grpc-endpoint/case.yaml @@ -0,0 +1,21 @@ +# GRPC rides on HTTP/2, so it is one of the four protocols a PUBLIC endpoint +# may serve. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + rpc: + containerPort: 8080 + protocol: GRPC + visibility: PUBLIC + health: + readiness: + path: /healthz + endpoint: rpc diff --git a/conformance/component/v1/structural/028-public-grpc-endpoint/metadata.json b/conformance/component/v1/structural/028-public-grpc-endpoint/metadata.json new file mode 100644 index 0000000..85a6938 --- /dev/null +++ b/conformance/component/v1/structural/028-public-grpc-endpoint/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-028-public-grpc-endpoint", + "phase": "structural", + "expected": "pass", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "GRPC is one of the protocols a PUBLIC endpoint may serve." +} diff --git a/conformance/component/v1/structural/029-endpoint-name-not-a-slug/case.yaml b/conformance/component/v1/structural/029-endpoint-name-not-a-slug/case.yaml new file mode 100644 index 0000000..fc4a46a --- /dev/null +++ b/conformance/component/v1/structural/029-endpoint-name-not-a-slug/case.yaml @@ -0,0 +1,17 @@ +# An endpoint name is referenced by probes and by platform defaults, and +# becomes a DNS label. A dot in one is a hazard rather than a matter of taste. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + web.api: + containerPort: 8080 + protocol: HTTP + visibility: PRIVATE diff --git a/conformance/component/v1/structural/029-endpoint-name-not-a-slug/diagnostics.json b/conformance/component/v1/structural/029-endpoint-name-not-a-slug/diagnostics.json new file mode 100644 index 0000000..0682289 --- /dev/null +++ b/conformance/component/v1/structural/029-endpoint-name-not-a-slug/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/workload/endpoints" + } +] diff --git a/conformance/component/v1/structural/029-endpoint-name-not-a-slug/metadata.json b/conformance/component/v1/structural/029-endpoint-name-not-a-slug/metadata.json new file mode 100644 index 0000000..fd05591 --- /dev/null +++ b/conformance/component/v1/structural/029-endpoint-name-not-a-slug/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-029-endpoint-name-not-a-slug", + "phase": "structural", + "expected": "fail", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "An endpoint name outside the slug grammar is rejected." +} diff --git a/conformance/component/v1/structural/030-two-public-endpoints/case.yaml b/conformance/component/v1/structural/030-two-public-endpoints/case.yaml new file mode 100644 index 0000000..82ba1f9 --- /dev/null +++ b/conformance/component/v1/structural/030-two-public-endpoints/case.yaml @@ -0,0 +1,36 @@ +# A component may front two public URLs. Each is addressed by name, which is +# what makes the second one no harder than the first. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + api: + containerPort: 8080 + protocol: HTTP + visibility: PUBLIC + console: + containerPort: 8081 + protocol: HTTP + visibility: PUBLIC + health: + readiness: + path: /healthz + endpoint: api + contract: + inputs: + consoleUrl: + schema: + type: STRING + platformDefault: + source: PUBLIC_URL + endpoint: console + ui: + label: Console URL + outputs: {} diff --git a/conformance/component/v1/structural/030-two-public-endpoints/metadata.json b/conformance/component/v1/structural/030-two-public-endpoints/metadata.json new file mode 100644 index 0000000..7697ef9 --- /dev/null +++ b/conformance/component/v1/structural/030-two-public-endpoints/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-030-two-public-endpoints", + "phase": "structural", + "expected": "pass", + "clause": "specifications/component/v1/spec.md#endpoints", + "summary": "Two PUBLIC endpoints validate when each public address names one." +} diff --git a/specifications/component/v1/examples/multi-endpoint.yaml b/specifications/component/v1/examples/multi-endpoint.yaml new file mode 100644 index 0000000..3671d22 --- /dev/null +++ b/specifications/component/v1/examples/multi-endpoint.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://schemas.musher.dev/component/v1/component.schema.json +# +# A service fronting two public URLs and one private port. Because more than +# one endpoint is PUBLIC, nothing here may say "the component's URL" — the +# readiness probe and the platform default each name the endpoint they mean. +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + ref: nginx:1.29.4-alpine + endpoints: + api: + containerPort: 8080 + protocol: HTTP + visibility: PUBLIC + console: + containerPort: 8081 + protocol: HTTP + visibility: PUBLIC + metrics: + containerPort: 9090 + protocol: HTTP + visibility: PRIVATE + health: + readiness: + path: /healthz + endpoint: api + contract: + inputs: + consoleUrl: + schema: + type: STRING + platformDefault: + source: PUBLIC_URL + endpoint: console + ui: + label: Console URL + target: + envVarKey: CONSOLE_URL + outputs: {} diff --git a/specifications/component/v1/schemas/dist/component.schema.json b/specifications/component/v1/schemas/dist/component.schema.json index a42462d..055178d 100644 --- a/specifications/component/v1/schemas/dist/component.schema.json +++ b/specifications/component/v1/schemas/dist/component.schema.json @@ -125,9 +125,38 @@ }, "ComponentEndpoint": { "additionalProperties": false, + "allOf": [ + { + "$comment": "spec.md §5.2. A PUBLIC endpoint is published at a URL by something that speaks the HTTP family, so TCP and UDP have no URL to publish. A bare conditional carrying no `type` declares no properties of its own, so lint.ts checkClosedObjects correctly skips it.", + "if": { + "properties": { + "visibility": { + "const": "PUBLIC" + } + }, + "required": [ + "visibility" + ] + }, + "then": { + "properties": { + "protocol": { + "enum": [ + "HTTP", + "HTTPS", + "WS", + "GRPC" + ] + } + } + } + } + ], "properties": { "containerPort": { - "description": "Port the workload listens on inside the container.", + "description": "Port the workload listens on inside the container. Below 1024 needs a capability the runtime grants, so spec.md §5.2 discourages it without rejecting it.", + "maximum": 65535, + "minimum": 1, "type": "integer" }, "protocol": { @@ -415,14 +444,10 @@ "type": "boolean" }, "platformDefault": { - "description": "Platform-derived default bound into the target env var — PUBLIC_HOSTNAME injects the component's own public endpoint hostname, PUBLIC_URL the full https URL of that endpoint; null when the value is user-supplied, wired, or generated.", + "description": "Default derived from the component's own addressing and bound into the target env var; null when the value is user-supplied, wired, or generated.", "anyOf": [ { - "enum": [ - "PUBLIC_HOSTNAME", - "PUBLIC_URL" - ], - "type": "string" + "$ref": "#/$defs/ComponentPlatformDefault" }, { "type": "null" @@ -619,13 +644,45 @@ "type": "object", "x-additionalPropertiesName": "outputName" }, + "ComponentPlatformDefault": { + "description": "Default derived from the component's own public addressing. Which endpoint it derives from is named rather than assumed, because spec.md §5.2 permits more than one PUBLIC endpoint.", + "additionalProperties": false, + "properties": { + "endpoint": { + "description": "Name of the PUBLIC endpoint the value is derived from. Null selects the primary endpoint spec.md §5.2 elects.", + "anyOf": [ + { + "pattern": "^[a-z][a-z0-9-]{0,61}[a-z0-9]$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, + "source": { + "description": "What is derived: PUBLIC_HOSTNAME for the endpoint's hostname alone, PUBLIC_URL for its full URL.", + "enum": [ + "PUBLIC_HOSTNAME", + "PUBLIC_URL" + ], + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, "ComponentProbe": { "additionalProperties": false, "properties": { "endpoint": { - "description": "Name of the endpoint whose port the probe targets. Null selects the primary endpoint.", + "description": "Name of the endpoint whose port the probe targets. Null selects the primary endpoint spec.md §5.2 elects.", "anyOf": [ { + "pattern": "^[a-z][a-z0-9-]{0,61}[a-z0-9]$", "type": "string" }, { @@ -1008,6 +1065,10 @@ "additionalProperties": { "$ref": "#/$defs/ComponentEndpoint" }, + "propertyNames": { + "$comment": "spec.md §5.2. An endpoint name is referenced by probes and by platform defaults, and becomes a DNS label, so it takes the slug grammar rather than an arbitrary key.", + "pattern": "^[a-z][a-z0-9-]{0,61}[a-z0-9]$" + }, "type": "object" }, "envVars": { diff --git a/specifications/component/v1/schemas/src/component.schema.json b/specifications/component/v1/schemas/src/component.schema.json index e14b4cc..5f1df05 100644 --- a/specifications/component/v1/schemas/src/component.schema.json +++ b/specifications/component/v1/schemas/src/component.schema.json @@ -125,9 +125,38 @@ }, "ComponentEndpoint": { "additionalProperties": false, + "allOf": [ + { + "$comment": "spec.md §5.2. A PUBLIC endpoint is published at a URL by something that speaks the HTTP family, so TCP and UDP have no URL to publish. A bare conditional carrying no `type` declares no properties of its own, so lint.ts checkClosedObjects correctly skips it.", + "if": { + "properties": { + "visibility": { + "const": "PUBLIC" + } + }, + "required": [ + "visibility" + ] + }, + "then": { + "properties": { + "protocol": { + "enum": [ + "HTTP", + "HTTPS", + "WS", + "GRPC" + ] + } + } + } + } + ], "properties": { "containerPort": { - "description": "Port the workload listens on inside the container.", + "description": "Port the workload listens on inside the container. Below 1024 needs a capability the runtime grants, so spec.md §5.2 discourages it without rejecting it.", + "maximum": 65535, + "minimum": 1, "type": "integer" }, "protocol": { @@ -417,18 +446,14 @@ "platformDefault": { "anyOf": [ { - "enum": [ - "PUBLIC_HOSTNAME", - "PUBLIC_URL" - ], - "type": "string" + "$ref": "#/$defs/ComponentPlatformDefault" }, { "type": "null" } ], "default": null, - "description": "Platform-derived default bound into the target env var — PUBLIC_HOSTNAME injects the component's own public endpoint hostname, PUBLIC_URL the full https URL of that endpoint; null when the value is user-supplied, wired, or generated." + "description": "Default derived from the component's own addressing and bound into the target env var; null when the value is user-supplied, wired, or generated." }, "schema": { "$ref": "#/$defs/ComponentValueSchema", @@ -619,12 +644,44 @@ "type": "object", "x-additionalPropertiesName": "outputName" }, + "ComponentPlatformDefault": { + "additionalProperties": false, + "description": "Default derived from the component's own public addressing. Which endpoint it derives from is named rather than assumed, because spec.md §5.2 permits more than one PUBLIC endpoint.", + "properties": { + "endpoint": { + "anyOf": [ + { + "pattern": "^[a-z][a-z0-9-]{0,61}[a-z0-9]$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of the PUBLIC endpoint the value is derived from. Null selects the primary endpoint spec.md §5.2 elects." + }, + "source": { + "description": "What is derived: PUBLIC_HOSTNAME for the endpoint's hostname alone, PUBLIC_URL for its full URL.", + "enum": [ + "PUBLIC_HOSTNAME", + "PUBLIC_URL" + ], + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, "ComponentProbe": { "additionalProperties": false, "properties": { "endpoint": { "anyOf": [ { + "pattern": "^[a-z][a-z0-9-]{0,61}[a-z0-9]$", "type": "string" }, { @@ -632,7 +689,7 @@ } ], "default": null, - "description": "Name of the endpoint whose port the probe targets. Null selects the primary endpoint." + "description": "Name of the endpoint whose port the probe targets. Null selects the primary endpoint spec.md §5.2 elects." }, "failureThreshold": { "default": 3, @@ -1008,6 +1065,10 @@ "$ref": "#/$defs/ComponentEndpoint" }, "description": "Endpoints the workload exposes, keyed by endpoint name. Only a SERVICE may declare one; on any other kind the mapping must be absent or empty.", + "propertyNames": { + "$comment": "spec.md §5.2. An endpoint name is referenced by probes and by platform defaults, and becomes a DNS label, so it takes the slug grammar rather than an arbitrary key.", + "pattern": "^[a-z][a-z0-9-]{0,61}[a-z0-9]$" + }, "type": "object" }, "envVars": { diff --git a/specifications/component/v1/spec.md b/specifications/component/v1/spec.md index b298bbe..cedab2c 100644 --- a/specifications/component/v1/spec.md +++ b/specifications/component/v1/spec.md @@ -115,9 +115,69 @@ release its consumers hold. ## 4. Metadata -> **TODO** — Define `metadata.version` semantics: monotonicity requirements, the -> relationship to the containing item's version, and whether reuse of a version -> number with different content is an error. +`metadata` carries `version` and nothing else. A component document has no +`slug`. A [blueprint](../../blueprint/v1/spec.md#identity) and its listing each +name the item they are two halves of; a component is not the item, and the name +it answers to is the stem of the file that holds it — which is what a repo-local +reference spells out in full +([blueprint §4.1](../../blueprint/v1/spec.md#component-reference)). Any other +property is `ERR_UNKNOWN_FIELD`, as [§2](#envelope) requires at every level. + +`version` is an integer, 1 or greater. It is REQUIRED and never defaulted, so +what a node deploys is a function of this file alone. + +**The version names a position in one component's lineage.** It is not a SemVer +triple and carries no compatibility meaning: nothing is derivable from the +distance between 2 and 7, and nothing is promised about how one version behaves +against another. It orders, and that is the whole of its job. + +**Two reference forms pin it, and only one writes it down.** + +| Reference form | What pins the version | +|---|---| +| Repo-local | The referenced document's own `metadata.version`. `componentVersion` MUST NOT be present. | +| Published | `componentVersion` on the node. | + +**A version is used once.** Each publication of a component MUST carry a version +strictly greater than the highest already published for that component. Gaps are +permitted — 1 to 7 is a release and not an error — but a version that does not +increase is rejected in the `capability` phase with `ERR_VERSION_NOT_MONOTONIC`. + +Reuse is the case the rule exists for. `componentVersion: 3` on a published node +is the whole of what that node deploys, and a registry that let 3 mean two +different documents would make the pin name nothing. The repo-local form has the +same problem one step removed: a blueprint that deployed version 3 last month +and version 3 today, with different bytes behind it, has no way to say so. + +**Why the phase is `capability`.** Deciding the rule needs to know what was +published before, which needs the catalog, which needs the network — and +[§7](#validation-layers) forbids the `parser`, `structural` and `semantic` +phases from requiring it. A client validating a file it has just written cannot +see the lineage and MUST NOT report this rule. Offline validation is therefore +exactly as strict as it was. + +Whether a registry treats an identical re-submission as a no-op rather than as a +publication is outside this contract. This document orders publications; it does +not define when two YAML files are the same document. + +**The version is not the item's version.** +[Blueprint §3](../../blueprint/v1/spec.md#identity) pins a blueprint to its +sibling listing, and neither is pinned to any component beneath it. The two +numbers count different things: an item's version counts releases of the item, a +component's counts releases of the component, and in the published form one +component is deployed by many items at once. + +A release of a component an item deploys SHOULD be accompanied by a release of +the item, because the listing describes what would be installed and a component +that has moved makes that description stale. It is a SHOULD and carries no +diagnostic: the disagreement is visible only across two revisions, and a +validator is handed one. + +**What v1 does not constrain.** Nothing orders one component's versions against +another's — two components in the same item sitting at 4 and 11 mean nothing +worth reading into. Nothing checks a version offline at all: `minimum: 1` is the +whole of the `structural` rule, and every other statement in this section is +either `capability` or a SHOULD. ## 5. Workload @@ -201,8 +261,82 @@ resolves. The floating-tag rule applies only to a reference carrying no digest. ### 5.2 Endpoints -> **TODO** — Port range, protocol/visibility interaction, and whether more than -> one `PUBLIC` endpoint is permitted. +`endpoints` is a mapping from endpoint name to one port the workload listens +on. `containerPort`, `protocol` and `visibility` are all REQUIRED — an endpoint +missing any of them describes a port nothing can route to. Only a `SERVICE` may +declare one; [§5](#workload) carries the rest of that rule. + +**An endpoint name is a reference.** A name MUST match +`^[a-z][a-z0-9-]{0,61}[a-z0-9]$` — the grammar +[blueprint §4.1](../../blueprint/v1/spec.md#component-reference) quotes for a +slug — and one that does not is rejected in the `structural` phase with +`ERR_INVALID_VALUE`. The name is not decoration: [§5.4](#health) points a probe +at one and [§6.1](#inputs) derives an address from one, so a dot or a space in +a name is a hazard rather than a matter of taste. The 63-character bound is a +DNS label, which is what the name becomes. + +**`containerPort` is an integer from 1 to 65535.** Outside that range there is +no port to bind. The bound is `structural` and carries `ERR_INVALID_VALUE`. + +A port below 1024 SHOULD NOT be used. Binding one needs a capability the runtime +grants to the container, and a workload that does not hold it fails at deploy +time with nothing in the document to blame. It stays advice rather than a +rejection because whether that grant exists is a fact about the runtime this +document cannot see, and a rule that rejects on a fact it cannot check is +guessing. Making it a MUST later rejects documents v1 accepts, and is therefore +breaking. + +**A `PUBLIC` endpoint is reached over HTTP.** `visibility: PUBLIC` publishes the +endpoint at an externally reachable URL, and what publishes it speaks the HTTP +family. `protocol` on a `PUBLIC` endpoint MUST therefore be one of `HTTP`, +`HTTPS`, `WS` or `GRPC`; a `TCP` or `UDP` endpoint MUST be `PRIVATE`. The rule +is `structural` and carries `ERR_INVALID_VALUE`. + +[§5.4](#health) is the second argument for it. A probe polls an HTTP path, and +`readiness` is REQUIRED for a `SERVICE` exposing a `PUBLIC` endpoint — so a +`PUBLIC` `TCP` endpoint would compel a probe it has no way to express. + +**A component MAY declare more than one `PUBLIC` endpoint**, and each one +publishes its own URL. A component fronting an API on one port and a console on +another is one component rather than two, and nothing about routing the second +is harder than routing the first. + +The consequence is a rule and not a caveat: **anything naming a public address +MUST name the endpoint it means.** Where two exist there is no such thing as +"the component's URL". [§6.1](#inputs) is where that bites, and where the +selector lives. + +**The primary endpoint.** A reference MAY omit the endpoint it targets — a +probe's `endpoint` and a platform default's both admit null — and the primary +endpoint is what null selects. It is: + +1. the workload's sole endpoint, where it declares exactly one; failing that +2. its sole `PUBLIC` endpoint, where it declares exactly one; failing that +3. nothing. + +Where it is nothing, a reference that omits the endpoint is rejected in the +`semantic` phase with `ERR_AMBIGUOUS_ENDPOINT`. The schema cannot express this +for the reason [§5.4](#health) gives: the endpoint names are mapping keys +elsewhere in the document. + +"Nothing" is reached two ways and both are rejected, though they read +differently to an author. A workload declaring several candidates has too many +and must choose. A workload declaring no endpoint at all has none, and a probe +on it polls a port that does not exist — which [§5](#workload) permits the +workload to be, since a `SERVICE` MAY declare no endpoint, but which no probe +can survive. + +**Why that is an error rather than a tiebreak.** Electing the first name in sort +order would give every document an answer, and would let a new endpoint called +`api` silently re-point a probe that has worked for a year. A rule that changes +what an unedited line means is the failure [§2](#envelope) rejects a misspelled +optional field to avoid. + +**What v1 does not constrain.** Two endpoints MAY declare the same +`containerPort`, and nothing says which of them anything routing to that port +should believe. That silence is a gap rather than a considered permission, and +is recorded here so a reader can tell the two apart. Closing it rejects +documents that validate today. ### 5.3 Environment variables @@ -225,10 +359,12 @@ polls an HTTP path. `successThreshold: 1`, `failureThreshold: 3`. `endpoint` names the endpoint whose port the probe targets; null selects the -primary endpoint. A probe naming an endpoint the workload does not declare is -rejected in the `semantic` phase with `ERR_UNKNOWN_ENDPOINT`. The schema cannot -express it — the endpoint names are mapping keys elsewhere in the document, and -JSON Schema cannot constrain a value against a sibling's keys. +primary endpoint [§5.2](#endpoints) elects, and is rejected with +`ERR_AMBIGUOUS_ENDPOINT` where that section elects none. A probe naming an +endpoint the workload does not declare is rejected with `ERR_UNKNOWN_ENDPOINT`. +Both are `semantic`, and the schema can express neither — the endpoint names are +mapping keys elsewhere in the document, and JSON Schema cannot constrain a value +against a sibling's keys. **`readiness` is REQUIRED for a `SERVICE` exposing at least one `PUBLIC` endpoint**, and OPTIONAL everywhere else, including on a `SERVICE` whose @@ -291,6 +427,28 @@ marked sensitive is echoed back into logs and interfaces. The value rides on a `USER` input because that is the slot the install form already reserves for it — the user simply does not have to type it. +**A platform default derives the value from the component's own addressing.** +`platformDefault` is OPTIONAL and null by default. Where it is present, `source` +is REQUIRED and selects what is derived — `PUBLIC_URL` for the full URL of a +public endpoint, `PUBLIC_HOSTNAME` for its hostname alone — and `endpoint` names +which endpoint it is derived from. + +`endpoint` is null by default and selects the primary endpoint +[§5.2](#endpoints) elects. Since a component MAY expose several `PUBLIC` +endpoints, one that does MUST name the endpoint here: null elects nothing there +and is rejected with `ERR_AMBIGUOUS_ENDPOINT`. + +Two further rules follow the name, both `semantic`. An endpoint the workload +does not declare is `ERR_UNKNOWN_ENDPOINT` — the same code and the same reason +as a probe's. An endpoint that is declared but `PRIVATE` is +`ERR_ENDPOINT_NOT_PUBLIC`: both sources derive an externally reachable address, +and a `PRIVATE` endpoint has none to give. + +**A platform default is not a `CONNECTION`.** The value comes from the +component's own workload, never from an upstream node, which is the same line +[§6.2](#outputs) draws around an output. `suppliedBy` is unconstrained by +`platformDefault` in v1 — a gap, recorded rather than described as a decision. + ### 6.2 Outputs `schema` and `valueFrom` are REQUIRED. @@ -372,13 +530,16 @@ different text and that is expected. | `ERR_INVALID_TYPE` | `structural` | A value has the wrong type. | | `ERR_INVALID_VALUE` | `structural` | A value violates a pattern, enum, or bound. | | `ERR_UNPINNED_IMAGE` | `semantic` | An image reference carries a floating tag. | -| `ERR_UNKNOWN_ENDPOINT` | `semantic` | A probe names an endpoint the workload does not declare. | +| `ERR_UNKNOWN_ENDPOINT` | `semantic` | A probe or a platform default names an endpoint the workload does not declare. | +| `ERR_AMBIGUOUS_ENDPOINT` | `semantic` | A reference omits the endpoint, and the workload elects no primary. | +| `ERR_ENDPOINT_NOT_PUBLIC` | `semantic` | A platform default deriving a public address names a `PRIVATE` endpoint. | +| `ERR_VERSION_NOT_MONOTONIC` | `capability` | A published component version is not greater than the lineage's current version. | The `parser` and `structural` rows are the shared envelope registry: the [blueprint](../../blueprint/v1/spec.md#diagnostics) and [listing](../../listing/v1/spec.md#diagnostics) families declare themselves -additions to this table rather than restating it. The two `semantic` codes are -this family's own. +additions to this table rather than restating it. The `semantic` and +`capability` rows are this family's own. ## 9. Conformance diff --git a/tools/src/conformance.ts b/tools/src/conformance.ts index 73dde3e..d667787 100644 --- a/tools/src/conformance.ts +++ b/tools/src/conformance.ts @@ -433,6 +433,10 @@ const UNCOVERED: ReadonlyMap = new Map([ 'ERR_UNKNOWN_COMPONENT', 'capability — resolving a published reference needs the catalog, and no phase this repository runs may reach the network', ], + [ + 'ERR_VERSION_NOT_MONOTONIC', + 'capability — comparing a version against the lineage it extends needs the catalog, and a fixture is one document with no previous release to be greater than', + ], ]) /** diff --git a/tools/src/semantic.ts b/tools/src/semantic.ts index 3522a4a..b13f4c9 100644 --- a/tools/src/semantic.ts +++ b/tools/src/semantic.ts @@ -113,26 +113,113 @@ function checkImageRef(document: Json, out: Diagnostic[]): void { } /** - * Component §5.4 — a probe's `endpoint` MUST name a declared endpoint. JSON - * Schema cannot: the names are mapping keys elsewhere in the document, and no - * keyword constrains a value against a sibling's keys. + * Component §5.2 — the endpoint a null reference selects. The sole endpoint + * where the workload declares exactly one, failing that its sole PUBLIC one, + * failing that nothing. + * + * §5.2 chose "nothing" over a sort-order tiebreak deliberately: a tiebreak + * lets a new endpoint named `api` silently re-point a probe that already works. */ -function checkProbeEndpoints(document: Json, out: Diagnostic[]): void { - const workload = child(child(document, 'spec'), 'workload') - const health = child(workload, 'health') - if (!isObject(health)) return +function primaryEndpoint(endpoints: Json | undefined): string | undefined { + const names = keysOf(endpoints) + if (names.length === 1) return names[0] + + const publicNames = names.filter( + (name) => child(child(endpoints, name), 'visibility') === 'PUBLIC', + ) + return publicNames.length === 1 ? publicNames[0] : undefined +} - const declared = new Set(keysOf(child(workload, 'endpoints'))) - for (const probe of keysOf(health)) { - // null selects the primary endpoint, so only a named one can be wrong. - const endpoint = asString(child(child(health, probe), 'endpoint')) - if (endpoint !== undefined && !declared.has(endpoint)) { +/** + * One place a document names an endpoint: a probe's `endpoint` (§5.4) or a + * platform default's (§6.1). `mustBePublic` is what separates them — both + * platform-default sources derive an externally reachable address. + */ +interface EndpointReference { + /** The raw value, so an explicit null and an absent key are one case. */ + readonly value: Json | undefined + readonly path: string + readonly subject: string + readonly mustBePublic: boolean +} + +/** + * Component §5.2, §5.4 and §6.1. JSON Schema can express none of this: the + * endpoint names are mapping keys elsewhere in the document, and no keyword + * constrains a value against a sibling's keys. + */ +function checkEndpointReference( + reference: EndpointReference, + endpoints: Json | undefined, + out: Diagnostic[], +): void { + const named = asString(reference.value) + + // Absent or null both select the primary, which §5.2 may elect to be nothing. + if (named === undefined) { + if (primaryEndpoint(endpoints) === undefined) { out.push({ - code: 'ERR_UNKNOWN_ENDPOINT', - path: `/spec/workload/health/${token(probe)}/endpoint`, - message: `probe targets endpoint "${endpoint}", which the workload does not declare`, + code: 'ERR_AMBIGUOUS_ENDPOINT', + path: reference.path, + message: `${reference.subject} names no endpoint, and the workload elects no primary`, }) } + return + } + + const declared = child(endpoints, named) + if (declared === undefined) { + out.push({ + code: 'ERR_UNKNOWN_ENDPOINT', + path: reference.path, + message: `${reference.subject} targets endpoint "${named}", which the workload does not declare`, + }) + return + } + + if (reference.mustBePublic && child(declared, 'visibility') !== 'PUBLIC') { + out.push({ + code: 'ERR_ENDPOINT_NOT_PUBLIC', + path: reference.path, + message: `${reference.subject} derives a public address from endpoint "${named}", which is PRIVATE`, + }) + } +} + +/** Every endpoint a component document names, in document order. */ +function endpointReferences(document: Json): EndpointReference[] { + const spec = child(document, 'spec') + const health = child(child(spec, 'workload'), 'health') + const inputs = child(child(spec, 'contract'), 'inputs') + const references: EndpointReference[] = [] + + for (const probe of keysOf(health)) { + references.push({ + value: child(child(health, probe), 'endpoint'), + path: `/spec/workload/health/${token(probe)}/endpoint`, + subject: `${probe} probe`, + mustBePublic: false, + }) + } + + for (const input of keysOf(inputs)) { + const platformDefault = child(child(inputs, input), 'platformDefault') + if (!isObject(platformDefault)) continue + references.push({ + value: child(platformDefault, 'endpoint'), + path: `/spec/contract/inputs/${token(input)}/platformDefault/endpoint`, + subject: `platform default on input "${input}"`, + mustBePublic: true, + }) + } + + return references +} + +function checkEndpointReferences(document: Json, out: Diagnostic[]): void { + const endpoints = child(child(child(document, 'spec'), 'workload'), 'endpoints') + for (const reference of endpointReferences(document)) { + checkEndpointReference(reference, endpoints, out) } } @@ -638,7 +725,7 @@ export function semanticDiagnostics( if (family.name === 'component') { checkImageRef(document, out) - checkProbeEndpoints(document, out) + checkEndpointReferences(document, out) } if (family.name === 'blueprint') { checkConnectionRoles(document, out)