From 2dc050aeb5d0e47f9108ebc45dd45361a6ba3e21 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sun, 20 Sep 2026 10:33:03 -0700 Subject: [PATCH] fix(spec): keep project contract at v1 --- .github/workflows/ci.yml | 2 +- README.md | 4 +- cmd/ob-docgen/main.go | 4 +- cmd/ob-docgen/main_test.go | 4 +- cmd/ob/commands_test.go | 4 +- cmd/ob/doctor_test.go | 4 +- cmd/ob/init.go | 2 +- cmd/ob/init_test.go | 2 +- cmd/ob/main_test.go | 4 +- cmd/ob/ops_contract_test.go | 4 +- cmd/ob/output_test.go | 8 +- cmd/ob/preview.go | 2 +- cmd/ob/preview_test.go | 6 +- cmd/ob/schema.go | 2 +- docs/README.md | 3 +- docs/onebox.run-v1.schema.json | 240 +- docs/onebox.run-v2.schema.json | 3119 ----------------- e2e/apps/README.md | 2 +- e2e/apps/authentik.yml | 2 +- e2e/apps/ghost.yml | 2 +- e2e/apps/gitea.yml | 2 +- e2e/apps/immich.yml | 2 +- e2e/apps/n8n.yml | 2 +- e2e/apps/paperless.yml | 2 +- e2e/apps/penpot.yml | 2 +- e2e/apps/rocketchat.yml | 2 +- e2e/apps/umami.yml | 2 +- e2e/apps/uptime-kuma.yml | 2 +- e2e/apps/vaultwarden.yml | 2 +- e2e/destroy_test.go | 2 +- e2e/network_ownership_test.go | 2 +- e2e/server_execution_test.go | 2 +- e2e/testdata/app/ob.yml | 2 +- e2e/testdata/postgres/ob.yml.tmpl | 2 +- e2e/testdata/worker/ob-broken.yml | 2 +- e2e/testdata/worker/ob.yml | 2 +- internal/app/backup_schema_test.go | 6 +- internal/app/canonical_test.go | 4 +- internal/app/compose_test.go | 4 +- internal/app/constraints.go | 5 - internal/app/contract_shapes_test.go | 2 +- internal/app/eject_test.go | 8 +- internal/app/ejection_contract_test.go | 2 +- internal/app/environment_model_test.go | 24 +- internal/app/external_schema_test.go | 6 +- internal/app/generate_test.go | 18 +- internal/app/health_timing_test.go | 8 +- internal/app/jsonschema.go | 4 +- internal/app/jsonschema_test.go | 20 +- internal/app/jump_config_test.go | 2 +- internal/app/load.go | 116 +- internal/app/load_test.go | 137 +- internal/app/names_test.go | 2 +- internal/app/naming_scope_test.go | 4 +- internal/app/preflight_test.go | 10 +- internal/app/purity_test.go | 14 +- internal/app/resolve.go | 1 - internal/app/resolve_test.go | 8 +- internal/app/route_test.go | 8 +- internal/app/schedule_test.go | 18 +- internal/app/secrets_graph_test.go | 16 +- internal/app/service_extensions_test.go | 12 +- internal/app/services_test.go | 20 +- internal/app/testdata/corpus/README.md | 2 +- .../testdata/corpus/ext-authentik-managed.yml | 2 +- .../app/testdata/corpus/ext-authentik.yml | 2 +- internal/app/testdata/corpus/ext-frigate.yml | 2 +- internal/app/testdata/corpus/ext-gitea.yml | 2 +- .../testdata/corpus/ext-immich-sourced.yml | 2 +- internal/app/testdata/corpus/ext-immich.yml | 2 +- internal/app/testdata/corpus/ext-n8n.yml | 2 +- .../app/testdata/corpus/ext-paperless.yml | 2 +- .../app/testdata/corpus/ext-plausible.yml | 2 +- internal/app/testdata/corpus/ext-umami.yml | 2 +- internal/app/testdata/corpus/goal.yml | 2 +- internal/app/testdata/corpus/monk.yml | 2 +- internal/app/testdata/corpus/pursue.yml | 2 +- internal/app/testdata/corpus/recast.yml | 2 +- internal/app/types.go | 11 +- internal/app/validate.go | 19 +- internal/app/workload_contract_test.go | 4 +- internal/engine/backup_identity_test.go | 2 +- internal/engine/deploy.go | 8 + internal/engine/deploy_test.go | 30 +- internal/engine/fixtures_test.go | 2 +- internal/engine/host_environment_test.go | 2 +- internal/engine/recovery.go | 2 +- internal/engine/recovery_test.go | 31 - internal/engine/resume_test.go | 2 +- .../engine/secret_generation_rolling_test.go | 2 +- internal/engine/secret_generation_test.go | 2 +- internal/engine/secretspush_test.go | 2 +- internal/engine/verify_injection_test.go | 2 +- internal/onebox/bootstrap_test.go | 2 +- internal/onebox/exec_test.go | 2 +- internal/onebox/jump_route_test.go | 4 +- internal/onebox/load_service_runtime_test.go | 2 +- internal/onebox/operation_graph_test.go | 4 +- internal/onebox/secrets_push_test.go | 4 +- internal/onebox/service_test.go | 6 +- internal/onebox/staging_secrets_test.go | 4 +- internal/onebox/workload_contract_test.go | 12 +- internal/onebox/workload_plan_test.go | 4 +- internal/proxy/proxy_test.go | 2 +- site/public/onebox.run-v1.schema.json | 240 +- site/public/onebox.run-v2.schema.json | 3119 ----------------- site/src/components/landing/Derivation.astro | 2 +- .../docs/guides/environment-variables.mdx | 2 +- site/src/content/docs/reference/cli.mdx | 6 +- .../docs/reference/fields/top-level.mdx | 2 +- site/src/content/docs/reference/policies.mdx | 4 +- .../content/docs/reference/project-file.mdx | 33 +- .../content/docs/start/reading-it-back.mdx | 4 +- site/src/content/docs/status/capabilities.mdx | 2 +- site/src/pages/llms.txt.ts | 2 +- 115 files changed, 507 insertions(+), 7037 deletions(-) delete mode 100644 docs/onebox.run-v2.schema.json delete mode 100644 site/public/onebox.run-v2.schema.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05058997..271f1a2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,7 @@ jobs: run: | ./ob-smoke${{ runner.os == 'Windows' && '.exe' || '' }} version ./ob-smoke${{ runner.os == 'Windows' && '.exe' || '' }} --help - ./ob-smoke${{ runner.os == 'Windows' && '.exe' || '' }} schema --out "${{ runner.temp }}/onebox.run-v2.schema.json" + ./ob-smoke${{ runner.os == 'Windows' && '.exe' || '' }} schema --out "${{ runner.temp }}/onebox.run-v1.schema.json" e2e: name: End-to-end (Docker) diff --git a/README.md b/README.md index d5db7c19..3a8d3390 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ Starting from an existing Compose project, `ob init` writes the first draft. This is a complete single-workload project: ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json -api_version: onebox.run/v2 +# yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v1.schema.json +api_version: onebox.run/v1 app: shop environments: production: diff --git a/cmd/ob-docgen/main.go b/cmd/ob-docgen/main.go index 4a9efad3..9ad4bf5e 100644 --- a/cmd/ob-docgen/main.go +++ b/cmd/ob-docgen/main.go @@ -9,7 +9,7 @@ // // So this program is the only writer of `site/src/content/docs/reference/` // — the field pages, `drivers.mdx`, `errors.mdx` and `cli.mdx` — and of the -// schema published at `site/public/onebox.run-v2.schema.json`. Those pages carry +// schema published at `site/public/onebox.run-v1.schema.json`. Those pages carry // a generated marker, // which `--check` reads in both directions: it fails when a page differs from // what this binary would produce, and when a marked page survives that no @@ -67,7 +67,7 @@ func main() { os.Exit(1) } schema = append(schema, '\n') - publicFiles := map[string]string{"onebox.run-v2.schema.json": string(schema)} + publicFiles := map[string]string{"onebox.run-v1.schema.json": string(schema)} if check { if err := verify(out, files); err != nil { diff --git a/cmd/ob-docgen/main_test.go b/cmd/ob-docgen/main_test.go index a591e295..b0ee367b 100644 --- a/cmd/ob-docgen/main_test.go +++ b/cmd/ob-docgen/main_test.go @@ -318,12 +318,12 @@ func TestPublishedSchemaMatchesTheCheckedInCopy(t *testing.T) { } // Skipping on a read failure would turn "someone moved the file" into a // passing test, which is the drift this exists to catch. - onDisk, err := os.ReadFile(filepath.Join("..", "..", "docs", "onebox.run-v2.schema.json")) + onDisk, err := os.ReadFile(filepath.Join("..", "..", "docs", "onebox.run-v1.schema.json")) if err != nil { t.Fatalf("the checked-in schema must be readable: %v", err) } if strings.TrimSpace(string(generated)) != strings.TrimSpace(string(onDisk)) { - t.Error("the published schema differs from docs/onebox.run-v2.schema.json") + t.Error("the published schema differs from docs/onebox.run-v1.schema.json") } } diff --git a/cmd/ob/commands_test.go b/cmd/ob/commands_test.go index 388a8535..358ef3c6 100644 --- a/cmd/ob/commands_test.go +++ b/cmd/ob/commands_test.go @@ -63,7 +63,7 @@ func writeProject(t *testing.T) string { t.Helper() dir := t.TempDir() obYAML := ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: demo environments: { production: { server: deploy@example.invalid } } workloads: @@ -102,7 +102,7 @@ func TestValidateOK(t *testing.T) { func TestPreflightBlocksDeploy(t *testing.T) { dir := writeProject(t) obYAML := ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: demo environments: { production: { server: deploy@example.invalid } } workloads: diff --git a/cmd/ob/doctor_test.go b/cmd/ob/doctor_test.go index d91565a2..124eb52a 100644 --- a/cmd/ob/doctor_test.go +++ b/cmd/ob/doctor_test.go @@ -31,7 +31,7 @@ func doctorTestDependencies(t *testing.T) doctorDependencies { oldBinary := filepath.Join(oldDir, "ob") currentBinary := filepath.Join(currentDir, "ob") cfg := &app.Spec{ - APIVersion: "onebox.run/v2", + APIVersion: "onebox.run/v1", Name: "demo", Environments: map[string]app.Environment{ "production": { @@ -195,7 +195,7 @@ func TestDoctorReportsIncompatibleProjectPolicy(t *testing.T) { deps := doctorTestDependencies(t) deps.loadConfig = func(string) (*app.Spec, error) { return &app.Spec{ - APIVersion: "onebox.run/v2", + APIVersion: "onebox.run/v1", Environments: map[string]app.Environment{ "production": {Policy: app.Policy{MinOneboxVersion: "v2027.1.0"}}, }, diff --git a/cmd/ob/init.go b/cmd/ob/init.go index fdc38ac2..01eb63e6 100644 --- a/cmd/ob/init.go +++ b/cmd/ob/init.go @@ -98,7 +98,7 @@ func runInit(ctx context.Context, cmd *cobra.Command, g *globalFlags) error { // errors from the moment the file exists rather than after someone finds // out it could. fmt.Fprintf(&b, "# yaml-language-server: $schema=%s\n", app.SchemaID) - b.WriteString("api_version: onebox.run/v2\n") + b.WriteString("api_version: onebox.run/v1\n") fmt.Fprintf(&b, "app: %s\n", application) b.WriteString("environments:\n production:\n server: deploy@CHANGE-ME\n") b.WriteString("workloads:\n") diff --git a/cmd/ob/init_test.go b/cmd/ob/init_test.go index 18f9513f..fc181bce 100644 --- a/cmd/ob/init_test.go +++ b/cmd/ob/init_test.go @@ -48,7 +48,7 @@ func TestInitClassifiesAndDoctors(t *testing.T) { } y := string(b) for _, want := range []string{ - "api_version: onebox.run/v2", + "api_version: onebox.run/v1", "server: deploy@CHANGE-ME", "workloads:", "role: application", diff --git a/cmd/ob/main_test.go b/cmd/ob/main_test.go index bc6e96f1..e05d458a 100644 --- a/cmd/ob/main_test.go +++ b/cmd/ob/main_test.go @@ -8,14 +8,14 @@ import ( "testing" ) -const mainTestProject = `api_version: onebox.run/v2 +const mainTestProject = `api_version: onebox.run/v1 app: demo environments: {production: {server: deploy@example.invalid}} image: nginx:1.27 proxy: {kind: none} ` -const mainTestBuildProject = `api_version: onebox.run/v2 +const mainTestBuildProject = `api_version: onebox.run/v1 app: demo environments: {production: {server: deploy@example.invalid}} workloads: diff --git a/cmd/ob/ops_contract_test.go b/cmd/ob/ops_contract_test.go index be35bba2..105019ab 100644 --- a/cmd/ob/ops_contract_test.go +++ b/cmd/ob/ops_contract_test.go @@ -24,7 +24,7 @@ func writeOpsContractProject(t *testing.T, dir string, encrypted bool) string { } } path := filepath.Join(dir, "project.yml") - if err := os.WriteFile(path, []byte(`api_version: onebox.run/v2 + if err := os.WriteFile(path, []byte(`api_version: onebox.run/v1 app: shop environments: production: {server: deploy@example.invalid} @@ -169,7 +169,7 @@ func TestDestroyConfirmationMismatchIsCancelledBeforeTargetContact(t *testing.T) func TestServiceLogsAndExecNDJSONTagChannelsAndTargetKind(t *testing.T) { dir := t.TempDir() config := filepath.Join(dir, "project.yml") - if err := os.WriteFile(config, []byte(`api_version: onebox.run/v2 + if err := os.WriteFile(config, []byte(`api_version: onebox.run/v1 app: shop environments: production: {server: deploy@example.invalid} diff --git a/cmd/ob/output_test.go b/cmd/ob/output_test.go index d9ec13e5..eb23a2af 100644 --- a/cmd/ob/output_test.go +++ b/cmd/ob/output_test.go @@ -314,7 +314,7 @@ func TestStructuredDeployRequiresApprovalArtifactWithoutPrompting(t *testing.T) // failure appears at the consumer rather than here. func TestStructuredOutputCarriesNoDiagnostics(t *testing.T) { dir := t.TempDir() - writeFile(t, dir, "ob.yml", `api_version: onebox.run/v2 + writeFile(t, dir, "ob.yml", `api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} @@ -347,7 +347,7 @@ routes: // publish outlives the terminal it would have scrolled off. func TestStructuredOutputCarriesNoPlaintextSecret(t *testing.T) { dir := t.TempDir() - writeFile(t, dir, "ob.yml", `api_version: onebox.run/v2 + writeFile(t, dir, "ob.yml", `api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} @@ -424,7 +424,7 @@ func TestCommandGroupsValidateOutputBeforeRenderingHelp(t *testing.T) { func TestEjectStructuredOutputIsVersioned(t *testing.T) { for _, mode := range []string{"json"} { dir := t.TempDir() - writeFile(t, dir, "ob.yml", `api_version: onebox.run/v2 + writeFile(t, dir, "ob.yml", `api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} @@ -455,7 +455,7 @@ image: nginx func TestStructuredReadFailuresEmitTypedSafeRecords(t *testing.T) { dir := t.TempDir() - writeFile(t, dir, "ob.yml", `api_version: onebox.run/v2 + writeFile(t, dir, "ob.yml", `api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} diff --git a/cmd/ob/preview.go b/cmd/ob/preview.go index f9db016e..7c2783ea 100644 --- a/cmd/ob/preview.go +++ b/cmd/ob/preview.go @@ -31,7 +31,7 @@ func addPreviewCommand(root *cobra.Command, g *globalFlags) { cmd := &cobra.Command{ Use: "preview", Short: "render the runtime the declarative contract generates (no target, no changes)", - Long: "Load an onebox.run/v2 project, resolve the environment's overrides, and print\n" + + Long: "Load an onebox.run/v1 project, resolve the environment's overrides, and print\n" + "the Compose runtime Onebox would generate, with its content digest.\n\n" + "Nothing is contacted and nothing is written. Environment values are redacted:\n" + "a preview must never put a secret on a terminal.", diff --git a/cmd/ob/preview_test.go b/cmd/ob/preview_test.go index edae6afb..9e1d6eaa 100644 --- a/cmd/ob/preview_test.go +++ b/cmd/ob/preview_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -const previewProject = `api_version: onebox.run/v2 +const previewProject = `api_version: onebox.run/v1 app: demo environments: production: {server: root@1.2.3.4} @@ -79,7 +79,7 @@ func TestPreviewAppliesEnvironmentOverrides(t *testing.T) { // wrong, where, and what to run. func TestPreviewFailureIsActionable(t *testing.T) { dir := t.TempDir() - writeFile(t, dir, "ob.yml", `api_version: onebox.run/v2 + writeFile(t, dir, "ob.yml", `api_version: onebox.run/v1 app: demo environments: {production: {server: h}} workloads: {web: {role: application, build: ., routes: [{hostname: d.example.com, port: 80}]}} @@ -133,7 +133,7 @@ func dirEntries(t *testing.T, dir string) int { func TestEjectPicksAFreeName(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "compose.yaml", "services:\n db: {image: postgres}\n") - writeFile(t, dir, "ob.yml", `api_version: onebox.run/v2 + writeFile(t, dir, "ob.yml", `api_version: onebox.run/v1 app: ledger environments: {production: {server: root@1.2.3.4}} workloads: diff --git a/cmd/ob/schema.go b/cmd/ob/schema.go index eae87425..ab1aa36d 100644 --- a/cmd/ob/schema.go +++ b/cmd/ob/schema.go @@ -23,7 +23,7 @@ func addSchemaCommand(root *cobra.Command, g *globalFlags) { cmd := &cobra.Command{ Use: "schema", Short: "print the JSON Schema for the project file, for editors", - Long: "Write the JSON Schema for the `onebox.run/v2` project file.\n\n" + + Long: "Write the JSON Schema for the `onebox.run/v1` project file.\n\n" + "Reference it from the first line of a project so an editor can offer\n" + "completion, hover documentation and inline errors:\n\n" + " # yaml-language-server: $schema=" + app.SchemaID + "\n\n" + diff --git a/docs/README.md b/docs/README.md index b93088bb..ee1be826 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,8 +6,7 @@ the repository rather than to a reader. | Path | What it is | |---|---| -| [`onebox.run-v2.schema.json`](onebox.run-v2.schema.json) | The current JSON Schema for the project file. Generated from the Go model by `ob schema` and tested byte-for-byte against it. `app.SchemaID` points at this path on `main`, and `ob init` writes that URL onto the first line of every scaffolded project. | -| [`onebox.run-v1.schema.json`](onebox.run-v1.schema.json) | The frozen v1 schema, retained so the stable schema URL in existing v1 projects continues to resolve. | +| [`onebox.run-v1.schema.json`](onebox.run-v1.schema.json) | The current JSON Schema for the project file. Generated from the Go model by `ob schema` and tested byte-for-byte against it. `app.SchemaID` points at this path on `main`, and `ob init` writes that URL onto the first line of every scaffolded project. | | [`product.md`](product.md) | Product direction. Not an implementation contract, and not a capability list. | ## Where the user documentation went diff --git a/docs/onebox.run-v1.schema.json b/docs/onebox.run-v1.schema.json index df4232c1..9a1182b1 100644 --- a/docs/onebox.run-v1.schema.json +++ b/docs/onebox.run-v1.schema.json @@ -58,11 +58,6 @@ "compose" ] }, - { - "required": [ - "domain" - ] - }, { "required": [ "port" @@ -577,14 +572,6 @@ }, "type": "object" }, - "domain": { - "description": "Domain shorthand for one HTTPS route; requires port and cannot be combined with routes. Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "examples": [ - "shop.example.com" - ], - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$", - "type": "string" - }, "environments": { "additionalProperties": { "additionalProperties": false, @@ -1154,7 +1141,7 @@ "type": "object" }, "port": { - "description": "Container port used with domain shorthand and as the default HTTP health port.", + "description": "Default container port used by HTTP health checks.", "examples": [ 3000 ], @@ -1302,12 +1289,12 @@ { "if": { "properties": { - "domain": { + "hostname": { "const": "*" } }, "required": [ - "domain" + "hostname" ] }, "then": { @@ -1327,55 +1314,74 @@ "tls" ] } - } - ], - "oneOf": [ + }, { - "not": { + "if": { + "properties": { + "hostname": { + "pattern": "^\\*\\." + } + }, "required": [ - "wildcard_suffix" + "hostname" ] }, - "required": [ - "domain" - ] + "then": { + "properties": { + "protocol": { + "const": "http" + } + } + } }, { - "not": { + "if": { + "properties": { + "tls": { + "const": "passthrough" + } + }, "required": [ - "domain" + "tls" ] }, - "required": [ - "wildcard_suffix" - ] + "then": { + "properties": { + "protocol": { + "const": "tcp" + } + }, + "required": [ + "protocol" + ] + } } ], "patternProperties": { "^x-": {} }, "properties": { - "domain": { + "entrypoint": { + "default": "websecure", + "description": "Named proxy listener used for the route.", + "type": "string" + }, + "hostname": { "anyOf": [ { - "description": "Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$" + "maxLength": 253, + "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" }, { "const": "*" } ], - "description": "Exact DNS name matched by the proxy. Mutually exclusive with wildcard_suffix.", + "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", "examples": [ "shop.example.com" ], "type": "string" }, - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, "middlewares": { "description": "Ordered provider-qualified middleware references applied to this route.", "items": { @@ -1402,7 +1408,7 @@ }, "protocol": { "default": "http", - "description": "Routing protocol: http, tcp, or udp.", + "description": "Routing protocol: http or tcp.", "enum": [ "http", "tcp" @@ -1411,7 +1417,7 @@ }, "scheme": { "default": "http", - "description": "Backend connection scheme: http, https, h2c, tcp, or udp.", + "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", "enum": [ "http", "https", @@ -1428,17 +1434,11 @@ "none" ], "type": "string" - }, - "wildcard_suffix": { - "description": "DNS suffix whose immediate subdomains are matched. For example, example.com matches shop.example.com but not example.com or a.b.example.com. Mutually exclusive with domain.", - "examples": [ - "preview.example.com" - ], - "maxLength": 253, - "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$", - "type": "string" } }, + "required": [ + "hostname" + ], "type": "object" }, "type": "array" @@ -1853,55 +1853,6 @@ } ] }, - { - "not": { - "allOf": [ - { - "anyOf": [ - { - "required": [ - "domain" - ] - }, - { - "required": [ - "port" - ] - } - ] - }, - { - "required": [ - "routes" - ] - } - ] - } - }, - { - "if": { - "required": [ - "domain" - ] - }, - "then": { - "required": [ - "port" - ] - } - }, - { - "if": { - "required": [ - "port" - ] - }, - "then": { - "required": [ - "domain" - ] - } - }, { "not": { "allOf": [ @@ -2157,14 +2108,6 @@ ], "type": "string" }, - "domain": { - "description": "Domain shorthand for one HTTPS route; requires port and cannot be combined with routes. Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "examples": [ - "shop.example.com" - ], - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$", - "type": "string" - }, "drain": { "additionalProperties": false, "description": "Signal and timing used to remove a container from traffic before stopping it.", @@ -2665,7 +2608,7 @@ "type": "object" }, "port": { - "description": "Container port used with domain shorthand and as the default HTTP health port.", + "description": "Default container port used by HTTP health checks.", "examples": [ 3000 ], @@ -2774,12 +2717,12 @@ { "if": { "properties": { - "domain": { + "hostname": { "const": "*" } }, "required": [ - "domain" + "hostname" ] }, "then": { @@ -2799,55 +2742,74 @@ "tls" ] } - } - ], - "oneOf": [ + }, { - "not": { + "if": { + "properties": { + "hostname": { + "pattern": "^\\*\\." + } + }, "required": [ - "wildcard_suffix" + "hostname" ] }, - "required": [ - "domain" - ] + "then": { + "properties": { + "protocol": { + "const": "http" + } + } + } }, { - "not": { + "if": { + "properties": { + "tls": { + "const": "passthrough" + } + }, "required": [ - "domain" + "tls" ] }, - "required": [ - "wildcard_suffix" - ] + "then": { + "properties": { + "protocol": { + "const": "tcp" + } + }, + "required": [ + "protocol" + ] + } } ], "patternProperties": { "^x-": {} }, "properties": { - "domain": { + "entrypoint": { + "default": "websecure", + "description": "Named proxy listener used for the route.", + "type": "string" + }, + "hostname": { "anyOf": [ { - "description": "Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$" + "maxLength": 253, + "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" }, { "const": "*" } ], - "description": "Exact DNS name matched by the proxy. Mutually exclusive with wildcard_suffix.", + "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", "examples": [ "shop.example.com" ], "type": "string" }, - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, "middlewares": { "description": "Ordered provider-qualified middleware references applied to this route.", "items": { @@ -2874,7 +2836,7 @@ }, "protocol": { "default": "http", - "description": "Routing protocol: http, tcp, or udp.", + "description": "Routing protocol: http or tcp.", "enum": [ "http", "tcp" @@ -2883,7 +2845,7 @@ }, "scheme": { "default": "http", - "description": "Backend connection scheme: http, https, h2c, tcp, or udp.", + "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", "enum": [ "http", "https", @@ -2900,17 +2862,11 @@ "none" ], "type": "string" - }, - "wildcard_suffix": { - "description": "DNS suffix whose immediate subdomains are matched. For example, example.com matches shop.example.com but not example.com or a.b.example.com. Mutually exclusive with domain.", - "examples": [ - "preview.example.com" - ], - "maxLength": 253, - "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$", - "type": "string" } }, + "required": [ + "hostname" + ], "type": "object" }, "type": "array" diff --git a/docs/onebox.run-v2.schema.json b/docs/onebox.run-v2.schema.json deleted file mode 100644 index 5786c127..00000000 --- a/docs/onebox.run-v2.schema.json +++ /dev/null @@ -1,3119 +0,0 @@ -{ - "$id": "https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "additionalProperties": false, - "anyOf": [ - { - "properties": { - "workloads": { - "minProperties": 1 - } - }, - "required": [ - "workloads" - ] - }, - { - "anyOf": [ - { - "required": [ - "build" - ] - }, - { - "required": [ - "image" - ] - }, - { - "required": [ - "compose" - ] - } - ] - } - ], - "description": "One application, its workloads, the services it needs, and how a release rolls out.", - "not": { - "allOf": [ - { - "required": [ - "workloads" - ] - }, - { - "anyOf": [ - { - "required": [ - "build" - ] - }, - { - "required": [ - "image" - ] - }, - { - "required": [ - "compose" - ] - }, - { - "required": [ - "port" - ] - }, - { - "required": [ - "health" - ] - }, - { - "required": [ - "routes" - ] - } - ] - } - ] - }, - "patternProperties": { - "^x-": {} - }, - "properties": { - "api_version": { - "const": "onebox.run/v2", - "description": "Project contract version. Must be onebox.run/v2.", - "examples": [ - "onebox.run/v2" - ], - "type": "string" - }, - "app": { - "description": "Stable application name used in generated container, volume, network, and host paths. The application's name. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters, and may not begin \"ob-\" or be a name the host layout reserves.", - "examples": [ - "shop" - ], - "not": { - "anyOf": [ - { - "pattern": "^ob-" - }, - { - "const": "ob" - }, - { - "const": "onebox-proxy" - }, - { - "const": "_host" - } - ] - }, - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "backup_targets": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "bucket": { - "description": "Existing destination bucket used by this target. Expects a lower-case S3-compatible bucket name between 3 and 63 characters.", - "examples": [ - "onebox-backups" - ], - "pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", - "type": "string" - }, - "credentials": { - "additionalProperties": false, - "description": "Trusted encrypted-file entries containing destination credentials; values never appear in the project.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "access_key_entry": { - "description": "Variable name containing the destination access key. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "BACKUP_ACCESS_KEY_ID" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "file": { - "description": "Repository-relative encrypted credential file staged through the trusted secret flow. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "secrets/backup.env" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "default": "sops", - "description": "Trusted secret provider. Only sops is currently executable.", - "enum": [ - "sops" - ], - "type": "string" - }, - "secret_key_entry": { - "description": "Variable name containing the destination secret key. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "BACKUP_SECRET_ACCESS_KEY" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "session_token_entry": { - "description": "Optional variable name containing a temporary destination session token. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "BACKUP_SESSION_TOKEN" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - } - }, - "type": "object" - }, - "encryption": { - "additionalProperties": false, - "description": "Required encryption mode for each recovery kind this target may store.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cold": { - "description": "Encryption mode required for cold recovery: client-side or server-side.", - "enum": [ - "client-side", - "server-side" - ], - "type": "string" - }, - "pitr": { - "description": "Encryption mode required for point-in-time recovery: client-side or server-side.", - "enum": [ - "client-side", - "server-side" - ], - "type": "string" - }, - "snapshot": { - "description": "Encryption mode required for snapshot recovery: client-side or server-side.", - "enum": [ - "client-side", - "server-side" - ], - "type": "string" - } - }, - "type": "object" - }, - "endpoint": { - "description": "Destination API endpoint. HTTPS is required unless tls is explicitly insecure. Expects an http or https URL.", - "examples": [ - "https://objects.example.com" - ], - "pattern": "^https?://", - "type": "string" - }, - "failure_domain": { - "additionalProperties": false, - "description": "Operator-declared identity used to prove the destination does not share the protected host.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "host": { - "description": "Destination host identity used to refuse a target on the protected host. Expects a stable identifier of letters, digits, dots, colons, slashes, underscores and hyphens.", - "examples": [ - "backup-01.example.net" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$", - "type": "string" - }, - "identity": { - "description": "Stable operator-owned failure-domain identity, distinct from the protected host. Expects a stable identifier of letters, digits, dots, colons, slashes, underscores and hyphens.", - "examples": [ - "provider-a/us-east-1/account-42" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$", - "type": "string" - } - }, - "type": "object" - }, - "kind": { - "description": "Destination kind. Only s3-compatible is supported.", - "enum": [ - "s3-compatible" - ], - "examples": [ - "s3-compatible" - ], - "type": "string" - }, - "prefix": { - "description": "Non-secret object prefix reserved for Onebox backup data. Expects a relative object prefix with no empty leading component or shell metacharacter.", - "examples": [ - "production/shop" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,511}$", - "type": "string" - }, - "region": { - "description": "S3-compatible region when the endpoint requires one. Expects a lower-case S3-compatible region of letters, digits and hyphens.", - "examples": [ - "us-east-1" - ], - "pattern": "^[a-z0-9][a-z0-9-]{0,62}$", - "type": "string" - }, - "tls": { - "default": "verify", - "description": "Transport policy: verify, or skip-verify to accept a plaintext http endpoint.", - "enum": [ - "verify", - "skip-verify" - ], - "type": "string" - } - }, - "type": "object" - }, - "description": "User-owned off-host repositories available to service backup policies.", - "type": "object" - }, - "base_path": { - "default": "/var/lib/ob", - "description": "Absolute host directory beneath which Onebox stores application state and releases. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/srv/ob" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "build": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Build metadata for development. Production requires a resolved image supplied with --image.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "args": { - "additionalProperties": {}, - "description": "Build arguments supplied by the external build system.", - "type": "object" - }, - "context": { - "description": "Repository-relative build context. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "." - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "dockerfile": { - "description": "Repository-relative Dockerfile path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "Dockerfile" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "target": { - "description": "Named Dockerfile stage to build.", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Build metadata for development. Production requires a resolved image supplied with --image. Also accepts a build context path." - }, - "checks": { - "additionalProperties": false, - "description": "Assertions that must pass before a release becomes current unless marked advisory.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "exec": { - "description": "Commands run inside a named workload.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "run": { - "description": "Shell command verified inside the workload.", - "examples": [ - "test -f /srv/ready" - ], - "type": "string" - }, - "workload": { - "description": "Workload the command runs inside.", - "examples": [ - "web" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "http": { - "description": "HTTP paths probed inside a named workload.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "path": { - "description": "HTTP path verified inside the workload. Expects a path beginning with /.", - "examples": [ - "/healthz" - ], - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "port": { - "description": "Container port to probe.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "workload": { - "description": "Workload the path is probed inside.", - "examples": [ - "web" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "migrations": { - "description": "Migration revisions checked against captured job evidence.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "applied_revisions": { - "description": "Revisions the job must report as applied.", - "items": { - "type": "string" - }, - "type": "array" - }, - "job": { - "description": "Job workload whose captured evidence is checked.", - "examples": [ - "migrate" - ], - "type": "string" - }, - "provider": { - "description": "Migration tool that produced the revisions.", - "examples": [ - "alembic" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "url": { - "description": "External URLs probed from the operator side.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "contains": { - "description": "Text the response body must contain.", - "type": "string" - }, - "json_assertions": { - "description": "Scalar JSON response values that must match exactly.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "equals": { - "description": "Exact scalar value required at path." - }, - "path": { - "description": "Dot-separated path to a scalar value in the JSON response.", - "examples": [ - "service.ready" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "required_headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Exact response headers required for success.", - "type": "object" - }, - "status_codes": { - "description": "Allowed response status codes. A successful 2xx response is expected when omitted.", - "items": { - "maximum": 599, - "minimum": 100, - "type": "integer" - }, - "type": "array" - }, - "url": { - "description": "External HTTP or HTTPS URL verified from the operator side. Expects an http or https URL.", - "examples": [ - "https://shop.example.com/healthz" - ], - "pattern": "^https?://", - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - }, - "compose": { - "description": "Existing Compose service to adopt, as repository path#service. Expects a reference of the form path/to/compose.yaml#service.", - "examples": [ - "docker-compose.yml#web" - ], - "pattern": "^[^/#][^#]*#[a-zA-Z0-9._-]+$", - "type": "string" - }, - "deployment": { - "additionalProperties": false, - "description": "Release ordering, retention, and migration behavior.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "migration_policy": { - "default": "manual", - "description": "Policy for migration jobs during release and recovery.", - "enum": [ - "manual", - "auto", - "expand-only" - ], - "type": "string" - }, - "order": { - "description": "Explicit workload release order. Dependency order is derived when omitted.", - "items": { - "type": "string" - }, - "type": "array" - }, - "retain_releases": { - "default": 5, - "description": "Number of completed release directories retained for inspection and rollback.", - "minimum": 1, - "type": "integer" - } - }, - "type": "object" - }, - "environments": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "base_path": { - "description": "Environment-specific replacement for the project base_path. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/srv/ob" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "env_files": { - "description": "Default ordered environment-file list for application, worker, and job workloads in this environment.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative environment file path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "description": "Decryptor used before staging the file. The supported encrypted provider is sops.", - "enum": [ - "sops" - ], - "examples": [ - "sops" - ], - "type": "string" - } - }, - "required": [ - "file" - ], - "type": "object" - } - ], - "description": "Also accepts a path to an environment file." - }, - "type": "array" - }, - "jump": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Optional SSH jump host tunnelling the connection to this server, written as user@host or as an object with host, user, and port. Onebox verifies and authenticates both hops and never forwards the SSH agent.", - "examples": [ - "deploy@bastion.example.com" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "host": { - "description": "Jump host name or IP address.", - "examples": [ - "bastion.example.com" - ], - "type": "string" - }, - "port": { - "description": "SSH port on the jump host. The SSH default is used when omitted.", - "examples": [ - 2222 - ], - "type": "integer" - }, - "user": { - "description": "SSH user on the jump host. $USER is used when omitted; ob does not read ~/.ssh/config.", - "examples": [ - "deploy" - ], - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Optional SSH jump host tunnelling the connection to this server, written as user@host or as an object with host, user, and port. Onebox verifies and authenticates both hops and never forwards the SSH agent. Also accepts user@host or user@host:port." - }, - "overrides": { - "additionalProperties": false, - "description": "Environment-specific operational tuning. Overrides cannot change workload identity or data semantics.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "services": { - "additionalProperties": { - "additionalProperties": {}, - "type": "object" - }, - "description": "Allowed service tuning keyed by service name: resources and settings.", - "type": "object" - }, - "workloads": { - "additionalProperties": { - "additionalProperties": {}, - "type": "object" - }, - "description": "Allowed workload tuning keyed by workload name: replicas, resources, env, env_files, strategy, and routes.", - "type": "object" - } - }, - "type": "object" - }, - "policy": { - "additionalProperties": false, - "description": "Approval, runner compatibility, and migration-backup requirements for this environment.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "allow_agent_proposals": { - "default": true, - "description": "Declared permission for agent-authored proposals. The current CLI does not distinguish agent identity; execution remains approval-gated.", - "type": "boolean" - }, - "migrations": { - "additionalProperties": false, - "description": "What this environment requires of a release carrying migration risk.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "backup_key_material": { - "description": "Key-material identities the backup report must name.", - "examples": [ - [ - "BACKUP_ACCESS_KEY_ID" - ] - ], - "items": { - "type": "string" - }, - "type": "array" - }, - "backup_max_age": { - "default": "24h", - "description": "Maximum age of a backup report accepted for a migration. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "24h" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "require_backup": { - "default": false, - "description": "Require a plan-bound backup report before a release with migration risk.", - "type": "boolean" - }, - "require_restore_test": { - "default": false, - "description": "Require the backup report to state that a restore test succeeded.", - "type": "boolean" - } - }, - "type": "object" - }, - "min_onebox_version": { - "description": "Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.8.0.", - "examples": [ - "v2026.8.0" - ], - "pattern": "^v([1-9][0-9]{3})\\.([1-9]|1[0-2])\\.(0|[1-9][0-9]{0,18})$", - "type": "string" - }, - "min_plan_schema": { - "description": "Oldest executable plan schema accepted by this environment. Expects a plan schema identity such as onebox.run/executable-deploy-plan/v1alpha2.", - "examples": [ - "onebox.run/executable-deploy-plan/v1alpha2" - ], - "pattern": "^onebox\\.run/executable-deploy-plan/v[1-9][0-9]*((alpha|beta)[1-9][0-9]*)?$", - "type": "string" - }, - "require_approval": { - "default": true, - "description": "Require a plan-bound local confirmation before mutating this environment.", - "type": "boolean" - } - }, - "type": "object" - }, - "server": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "SSH server, written as user@host or as an object with host, user, and port.", - "examples": [ - "root@203.0.113.10" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "host": { - "description": "SSH hostname or IP address.", - "examples": [ - "203.0.113.10" - ], - "type": "string" - }, - "port": { - "description": "SSH port. The SSH default is used when omitted.", - "examples": [ - 2222 - ], - "type": "integer" - }, - "user": { - "description": "SSH user. $USER is used when omitted; ob does not read ~/.ssh/config.", - "examples": [ - "root" - ], - "type": "string" - } - }, - "type": "object" - } - ], - "description": "SSH server, written as user@host or as an object with host, user, and port. Also accepts user@host." - } - }, - "type": "object" - }, - "description": "Named environments, each naming the server it deploys to and the policy applied to it.", - "minProperties": 1, - "type": "object" - }, - "external_services": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "backup_owner": { - "description": "Operator or provider responsible for backup, restore, upgrades, credentials, and durability. Expects a stable operator or provider identity of letters, digits, dots, @, colons, slashes, underscores and hyphens.", - "examples": [ - "platform-team/rds" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._@:/-]{0,127}$", - "type": "string" - }, - "connection": { - "additionalProperties": false, - "description": "Trusted connection source and driver-shaped entry mapping.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "entries": { - "additionalProperties": { - "description": "Expects a variable name of letters, digits and underscores, not starting with a digit.", - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "description": "Maps driver connection parts such as host, port, user, password, database, or url to variable names in the trusted source.", - "type": "object" - }, - "source": { - "additionalProperties": false, - "description": "Trusted encrypted file containing the connection values.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative encrypted environment file staged through the trusted secret flow. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "secrets/production-db.env" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "default": "sops", - "description": "Trusted secret provider. Only sops is currently executable.", - "enum": [ - "sops" - ], - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "driver": { - "description": "Built-in connection shape used to validate and project this dependency.", - "enum": [ - "clickhouse", - "mariadb", - "meilisearch", - "minio", - "mongodb", - "mysql", - "nats", - "postgres", - "rabbitmq", - "redis", - "valkey" - ], - "examples": [ - "postgres" - ], - "type": "string" - }, - "probe": { - "additionalProperties": false, - "description": "Optional bounded read-only health observation; it never creates or repairs provider resources.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "kind": { - "default": "driver-health", - "description": "Read-only observation kind: driver-health.", - "enum": [ - "driver-health" - ], - "type": "string" - }, - "max_age": { - "default": "5m", - "description": "Maximum age of a probe observation bound into a plan. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "timeout": { - "default": "5s", - "description": "Maximum duration of one read-only probe. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "description": "Typed dependencies operated outside Onebox. Their connection projection is trusted, but their lifecycle and backup remain external.", - "type": "object" - }, - "health": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Readiness check used to gate rolling replacement.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "exec": { - "description": "Health command as a shell string or direct argument list." - }, - "http": { - "description": "HTTP path probed inside the container. Expects a path beginning with /.", - "examples": [ - "/healthz" - ], - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "interval": { - "default": "5s", - "description": "Delay between container health probes, at most 7d. Always written into the generated healthcheck, so the rollout's drain budget is computed from the value the container actually runs with. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "2s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "port": { - "description": "Container port probed by HTTP or TCP health checks.", - "examples": [ - 8080 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "retries": { - "default": 3, - "description": "Consecutive failed probes before the container is unhealthy. A draining container leaves rotation after this many probes, so it sets how long a rolling deploy waits for each replica.", - "examples": [ - 3 - ], - "type": "integer" - }, - "start_period": { - "default": "30s", - "description": "Startup grace period before failed probes count, at most 7d. Always written into the generated healthcheck, so writing down a fast probe interval does not call a booting container unhealthy. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "tcp": { - "default": false, - "description": "Probe the configured port by opening a TCP connection.", - "type": "boolean" - }, - "within": { - "description": "Maximum time a rollout waits for readiness, at most 7d. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "120s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Readiness check used to gate rolling replacement. Also accepts an HTTP health path." - }, - "hooks": { - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "local": { - "default": false, - "description": "Run on the operator machine instead of the server.", - "type": "boolean" - }, - "run": { - "description": "Command executed at the lifecycle seam.", - "examples": [ - "./scripts/notify.sh" - ], - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Also accepts the command to run." - }, - "description": "Lifecycle commands keyed by seam: bootstrap, pre_release, post_release, or post_deploy.", - "type": "object" - }, - "image": { - "anyOf": [ - { - "description": "Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - }, - { - "additionalProperties": false, - "description": "Container image source, written as a reference string or an object.", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "pull": { - "default": "missing", - "description": "When to fetch the image from the registry: missing fetches only what the host does not already hold, always fetches every release, never fetches at all and fails on a missing image.", - "enum": [ - "always", - "missing", - "never" - ], - "type": "string" - }, - "reference": { - "description": "Complete container image reference, optionally tagged or digest-pinned. Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Container image source, written as a reference string or an object. Also accepts an image reference." - }, - "notifications": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "format": { - "default": "text", - "description": "Notification payload format.", - "enum": [ - "text", - "json" - ], - "type": "string" - }, - "on": { - "default": [ - "success", - "failure" - ], - "description": "Operation outcomes that trigger this notification.", - "items": { - "enum": [ - "success", - "failure" - ], - "type": "string" - }, - "type": "array" - }, - "webhook": { - "description": "HTTP endpoint that receives outcome notifications.", - "examples": [ - "https://hooks.example.com/onebox" - ], - "type": "string" - } - }, - "type": "object" - }, - "description": "Named webhooks that receive selected operation and scheduled-job outcomes.", - "type": "object" - }, - "port": { - "description": "Default container port used by HTTP health checks.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "proxy": { - "additionalProperties": false, - "description": "Ownership and configuration of the host ingress proxy.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "config": { - "description": "Repository-relative proxy configuration directory. Dynamic YAML or TOML files extend Onebox's managed configuration. A managed DNS challenge may use a directory containing only .env for provider credentials. Including traefik.yml or traefik.yaml instead takes ownership of the static configuration, which must use the watched file-provider directory /etc/traefik/dynamic, must not enable the Docker provider, must define certificatesResolvers.letsencrypt for exact terminating routes, and must define the DNS-01 certificatesResolvers.onebox-wildcard for wildcard terminating routes. Dynamic files may not reuse Onebox-generated router or service names or redefine the managed onebox-compress middleware. Expects a path inside the repository, with no control character or shell metacharacter.", - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "dns_challenge": { - "additionalProperties": false, - "description": "Managed ACME DNS-01 challenge used to issue wildcard certificates. Provider credentials belong in proxy.config/.env; Onebox continues to own the static proxy configuration.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "provider": { - "description": "Traefik DNS challenge provider name. Its credential variables must be supplied through proxy.config/.env. Expects a lower-case Traefik DNS provider name such as cloudflare or route53.", - "examples": [ - "cloudflare" - ], - "pattern": "^[a-z][a-z0-9_-]*$", - "type": "string" - }, - "resolvers": { - "description": "DNS resolvers used to verify challenge propagation, written as host:port.", - "examples": [ - [ - "1.1.1.1:53" - ] - ], - "items": { - "description": "Expects a lower-case DNS name, IPv4 address, or bracketed IPv6 address followed by a port.", - "pattern": "^([a-z0-9]([a-z0-9.-]*[a-z0-9])?|\\[[0-9A-Fa-f:.]+\\]):[0-9]{1,5}$", - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "provider" - ], - "type": "object" - }, - "entrypoints": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "port": { - "description": "Host and proxy-container TCP port used by this listener.", - "examples": [ - 4317 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - } - }, - "type": "object" - }, - "description": "Additional named TCP listeners published by the managed proxy. Onebox adds them to its generated static configuration; a proxy.config containing custom traefik.yml or traefik.yaml must define matching Traefik entrypoints.", - "propertyNames": { - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$" - }, - "type": "object" - }, - "image": { - "description": "Container image used for the managed proxy. Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - }, - "kind": { - "default": "traefik-docker", - "description": "Proxy implementation, or none to disable routing.", - "enum": [ - "traefik-docker", - "none" - ], - "type": "string" - }, - "managed": { - "description": "Let Onebox converge the host-scoped proxy when routes are declared.", - "type": "boolean" - }, - "network": { - "default": "ob-ingress", - "description": "External container network shared with routed workloads; default and Onebox's derived application and service network names are reserved.", - "type": "string" - } - }, - "type": "object" - }, - "registries": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "password_env": { - "description": "Local environment-variable name containing the registry password or token. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "GHCR_TOKEN" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "server": { - "description": "Registry hostname, optionally with a port. Expects a host with an optional port and path, such as ghcr.io or registry.example.com:5000.", - "examples": [ - "ghcr.io" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9.-]*(:[0-9]{1,5})?(/[A-Za-z0-9._/-]*)?$", - "type": "string" - }, - "username": { - "description": "Registry login username. Expects a username of letters, digits and . _ @ + -.", - "pattern": "^[A-Za-z0-9][A-Za-z0-9._@+-]*$", - "type": "string" - } - }, - "type": "object" - }, - "description": "Named container registries and the environment variables holding their credentials.", - "type": "object" - }, - "routes": { - "description": "Ingress routes exposed by this workload.", - "items": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "hostname": { - "const": "*" - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - }, - "tls": { - "enum": [ - "none", - "passthrough" - ] - } - }, - "required": [ - "protocol", - "tls" - ] - } - }, - { - "if": { - "properties": { - "hostname": { - "pattern": "^\\*\\." - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "http" - } - } - } - }, - { - "if": { - "properties": { - "tls": { - "const": "passthrough" - } - }, - "required": [ - "tls" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - } - }, - "required": [ - "protocol" - ] - } - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, - "hostname": { - "anyOf": [ - { - "maxLength": 253, - "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" - }, - { - "const": "*" - } - ], - "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", - "examples": [ - "shop.example.com" - ], - "type": "string" - }, - "middlewares": { - "description": "Ordered provider-qualified middleware references applied to this route.", - "items": { - "description": "Expects a provider-qualified name such as secure-headers@file.", - "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*@[a-z][a-z0-9-]*$", - "type": "string" - }, - "type": "array" - }, - "path": { - "default": "/", - "description": "URL path prefix matched by an HTTP route. Expects a path beginning with /.", - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "port": { - "description": "Container port receiving routed traffic.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "protocol": { - "default": "http", - "description": "Routing protocol: http or tcp.", - "enum": [ - "http", - "tcp" - ], - "type": "string" - }, - "scheme": { - "default": "http", - "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", - "enum": [ - "http", - "https", - "h2c" - ], - "type": "string" - }, - "tls": { - "default": "terminate", - "description": "TLS handling: terminate, passthrough, or none.", - "enum": [ - "terminate", - "passthrough", - "none" - ], - "type": "string" - } - }, - "required": [ - "hostname" - ], - "type": "object" - }, - "type": "array" - }, - "runtime": { - "additionalProperties": false, - "description": "Project-wide environment files and local environment-file requirements.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "env_checks": { - "description": "Local environment-file assertions checked before planning or deploying.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative dotenv file whose declared keys are checked. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "present": { - "description": "Environment keys that must be declared but may be empty.", - "items": { - "type": "string" - }, - "type": "array" - }, - "require": { - "description": "Environment keys that must be declared with non-empty values.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "type": "array" - }, - "env_files": { - "description": "Project-wide ordered environment-file list for application, worker, and job workloads.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative environment file path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "description": "Decryptor used before staging the file. The supported encrypted provider is sops.", - "enum": [ - "sops" - ], - "examples": [ - "sops" - ], - "type": "string" - } - }, - "required": [ - "file" - ], - "type": "object" - } - ], - "description": "Also accepts a path to an environment file." - }, - "type": "array" - } - }, - "type": "object" - }, - "services": { - "additionalProperties": { - "anyOf": [ - { - "type": [ - "string", - "number", - "integer" - ] - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "backup": { - "additionalProperties": false, - "description": "Recovery intent for this service. Onebox selects the qualified native implementation; declaring intent alone does not establish backup.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "allow_downtime": { - "default": false, - "description": "Whether recurring backup operations may use the driver-declared stopped-service window.", - "type": "boolean" - }, - "drill": { - "additionalProperties": false, - "description": "Exact isolated restore-test schedule, proof age, and optional staging filesystem.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "max_age": { - "default": "7d", - "description": "Maximum age of the latest passing restore proof. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "7d" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "schedule": { - "additionalProperties": false, - "description": "Exact recurring isolated restore-test schedule.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cron": { - "description": "Five-field cron schedule translated to a host timer. Expects five cron fields.", - "examples": [ - "0 2 * * *" - ], - "pattern": "^[-0-9*/,A-Za-z ]+$", - "type": "string" - }, - "timezone": { - "default": "UTC", - "description": "IANA timezone used to interpret the cron schedule. Expects an IANA zone name such as UTC or Europe/Berlin.", - "examples": [ - "Europe/Berlin" - ], - "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$", - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "max_data_loss": { - "description": "Maximum tolerable interval between the latest recoverable point and failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "15m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "recovery_kind": { - "description": "Required recovery envelope: snapshot, pitr, or cold.", - "enum": [ - "snapshot", - "pitr", - "cold" - ], - "examples": [ - "pitr" - ], - "type": "string" - }, - "retention": { - "additionalProperties": false, - "description": "Portable minimum recovery history that the selected native driver must be able to preserve.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "keep": { - "default": 7, - "description": "Minimum number of independently recoverable base generations to retain.", - "examples": [ - 7 - ], - "minimum": 1, - "type": "integer" - }, - "window": { - "default": "7d", - "description": "Minimum continuous recovery history the native retention mapping must preserve. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "7d" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - }, - "schedule": { - "additionalProperties": false, - "description": "Exact recurring base-backup schedule.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cron": { - "description": "Five-field cron schedule translated to a host timer. Expects five cron fields.", - "examples": [ - "0 2 * * *" - ], - "pattern": "^[-0-9*/,A-Za-z ]+$", - "type": "string" - }, - "timezone": { - "default": "UTC", - "description": "IANA timezone used to interpret the cron schedule. Expects an IANA zone name such as UTC or Europe/Berlin.", - "examples": [ - "Europe/Berlin" - ], - "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$", - "type": "string" - } - }, - "type": "object" - }, - "target": { - "description": "Name of a project-level backup target. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "examples": [ - "offsite" - ], - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - } - }, - "type": "object" - }, - "driver": { - "description": "Built-in service driver. Defaults to the service map key. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "examples": [ - "postgres" - ], - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "features": { - "additionalProperties": false, - "description": "Capabilities Onebox must establish before application workloads run.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "extensions": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": {}, - "type": "object" - }, - "description": "PostgreSQL extensions Onebox installs in the managed application database before application migrations run.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9_-]*$" - }, - "type": "object" - } - }, - "type": "object" - }, - "persistence": { - "additionalProperties": false, - "description": "Data-lifetime declaration for this supporting service.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "mode": { - "default": "durable", - "description": "Data lifetime: durable, ephemeral, or external.", - "enum": [ - "durable", - "ephemeral", - "external" - ], - "type": "string" - } - }, - "type": "object" - }, - "resources": { - "additionalProperties": false, - "description": "Memory and CPU limits for this supporting service.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cpus": { - "description": "Container CPU limit expressed as a positive decimal count. Expects a number of CPUs such as 0.5 or 2.", - "examples": [ - "0.5" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?$", - "type": "string" - }, - "memory": { - "description": "Container memory limit. Expects a size such as 512MB or 1.5GB.", - "examples": [ - "512MB" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?(B|KB|MB|GB|TB)$", - "type": "string" - } - }, - "type": "object" - }, - "settings": { - "additionalProperties": {}, - "description": "Driver-specific settings validated by the selected service driver.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9_-]*$" - }, - "type": "object" - }, - "version": { - "description": "Driver version or image tag to run.", - "examples": [ - "17" - ] - }, - "volumes": { - "description": "Additional driver-defined persistent volume names.", - "items": { - "description": "Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - ], - "description": "Also accepts the version to run." - }, - "description": "Supporting services managed outside application releases, such as databases and caches.", - "type": "object" - }, - "workloads": { - "additionalProperties": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "required": [ - "execution" - ] - }, - "then": { - "not": { - "required": [ - "compose" - ] - }, - "properties": { - "data_effect": { - "const": "none" - }, - "deployment_phase": { - "const": "none" - }, - "operator_run": { - "const": "allowed" - } - }, - "required": [ - "schedule", - "data_effect" - ] - } - }, - { - "oneOf": [ - { - "required": [ - "build" - ] - }, - { - "required": [ - "image" - ] - }, - { - "required": [ - "compose" - ] - } - ] - }, - { - "not": { - "allOf": [ - { - "required": [ - "published_ports" - ] - }, - { - "anyOf": [ - { - "properties": { - "strategy": { - "const": "rolling" - } - }, - "required": [ - "strategy" - ] - }, - { - "allOf": [ - { - "not": { - "required": [ - "strategy" - ] - } - }, - { - "required": [ - "health" - ] - }, - { - "anyOf": [ - { - "properties": { - "role": { - "const": "application" - } - }, - "required": [ - "role" - ] - }, - { - "not": { - "required": [ - "role" - ] - } - } - ] - } - ] - } - ] - } - ] - } - }, - { - "if": { - "properties": { - "persistence": { - "anyOf": [ - { - "properties": { - "mode": { - "const": "durable" - } - }, - "required": [ - "mode" - ] - }, - { - "not": { - "required": [ - "mode" - ] - } - } - ] - } - }, - "required": [ - "persistence" - ] - }, - "then": { - "properties": { - "replicas": { - "maximum": 1 - } - } - } - }, - { - "else": { - "not": { - "anyOf": [ - { - "required": [ - "deployment_phase" - ] - }, - { - "required": [ - "operator_run" - ] - }, - { - "required": [ - "data_effect" - ] - }, - { - "required": [ - "schedule" - ] - }, - { - "required": [ - "inputs" - ] - }, - { - "required": [ - "execution" - ] - } - ] - } - }, - "if": { - "properties": { - "role": { - "const": "job" - } - }, - "required": [ - "role" - ] - }, - "then": { - "required": [ - "data_effect" - ] - } - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "build": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Build metadata for development. Production requires a resolved image supplied with --image.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "args": { - "additionalProperties": {}, - "description": "Build arguments supplied by the external build system.", - "type": "object" - }, - "context": { - "description": "Repository-relative build context. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "." - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "dockerfile": { - "description": "Repository-relative Dockerfile path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "Dockerfile" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "target": { - "description": "Named Dockerfile stage to build.", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Build metadata for development. Production requires a resolved image supplied with --image. Also accepts a build context path." - }, - "command": { - "anyOf": [ - { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, - { - "description": "Container command as a shell string or argument list.", - "examples": [ - "./bin/server" - ] - } - ], - "description": "Container command as a shell string or argument list. Also accepts a command line or argument list." - }, - "compose": { - "description": "Existing Compose service to adopt, as repository path#service. Expects a reference of the form path/to/compose.yaml#service.", - "examples": [ - "docker-compose.yml#web" - ], - "pattern": "^[^/#][^#]*#[a-zA-Z0-9._-]+$", - "type": "string" - }, - "data_effect": { - "description": "Job data impact used by rollback and abort gates.", - "enum": [ - "none", - "migration", - "destructive", - "unknown" - ], - "examples": [ - "migration" - ], - "type": "string" - }, - "deployment_phase": { - "default": "none", - "description": "Deployment phase for this job: none, pre_release, or post_release.", - "enum": [ - "none", - "pre_release", - "post_release" - ], - "type": "string" - }, - "drain": { - "additionalProperties": false, - "description": "Signal and timing used to remove a container from traffic before stopping it.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "grace": { - "description": "Maximum graceful-shutdown time before forced termination, at most 7d. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "signal": { - "default": "TERM", - "description": "Signal sent to begin graceful shutdown. Expects a signal name such as TERM or QUIT.", - "pattern": "^[A-Z][A-Z0-9]*$", - "type": "string" - }, - "wait": { - "description": "Maximum drain window before shutdown continues, at most 7d. Recreate workloads continue sooner when every old container exits. Rolling workloads wait the full interval before stopping each container when their health check supports drain guarding. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "10s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - }, - "entrypoint": { - "anyOf": [ - { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, - { - "description": "Container entrypoint as a string or argument list." - } - ], - "description": "Container entrypoint as a string or argument list. Also accepts an entrypoint or argument list." - }, - "env": { - "additionalProperties": {}, - "description": "Literal container environment values. Managed-service credential variables cannot be overridden.", - "type": "object" - }, - "env_files": { - "description": "Workload-specific ordered environment-file list. Replaces broader defaults when present.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative environment file path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "description": "Decryptor used before staging the file. The supported encrypted provider is sops.", - "enum": [ - "sops" - ], - "examples": [ - "sops" - ], - "type": "string" - } - }, - "required": [ - "file" - ], - "type": "object" - } - ], - "description": "Also accepts a path to an environment file." - }, - "type": "array" - }, - "execution": { - "additionalProperties": false, - "description": "Opt-in durable scheduled execution. Requires a native operator-runnable phase-none job with data_effect none. Stores non-secret checkpoints on the host and permits explicit same-release resume.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "retention": { - "default": "168h", - "description": "Time from creation during which an unsuccessful execution may be resumed, at most 30d. Active executions remain protected. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "steps": { - "description": "Optional ordered steps using this job's image and entrypoint. Omit to execute the job command as one step. At most 32 steps.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "command": { - "description": "Argument vector passed to the job image's entrypoint. No shell evaluation is performed.", - "items": { - "type": "string" - }, - "maxItems": 128, - "minItems": 1, - "type": "array" - }, - "id": { - "description": "Unique stable step identifier, used by output references. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "inputs": { - "additionalProperties": { - "type": "string" - }, - "description": "Environment variables populated from a preceding step's declared output, written as step.OUTPUT.", - "propertyNames": { - "pattern": "^[A-Z][A-Z0-9_]*$" - }, - "type": "object" - }, - "outputs": { - "description": "Required string keys in the JSON object written to ONEBOX_OUTPUT_FILE. Values are non-secret, at most 4096 bytes each and 16384 bytes total.", - "items": { - "description": "Expects upper-case letters, digits and underscores, starting with a letter.", - "pattern": "^[A-Z][A-Z0-9_]*$", - "type": "string" - }, - "maxItems": 32, - "type": "array", - "uniqueItems": true - }, - "retry": { - "additionalProperties": false, - "description": "Per-step retry policy; defaults to schedule.retry. All steps and backoff share the activation timeout.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "attempts": { - "default": 1, - "description": "Total attempts including the first, 1 to 10.", - "examples": [ - 3 - ], - "maximum": 10, - "minimum": 1, - "type": "integer" - }, - "backoff": { - "default": "30s", - "description": "Sleep before the second attempt; it doubles after each failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "1m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "max_backoff": { - "default": "10m", - "description": "Upper bound for the doubling sleep. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - }, - "required": [ - "id", - "command" - ], - "type": "object" - }, - "maxItems": 32, - "type": "array" - } - }, - "type": "object" - }, - "extra_hosts": { - "description": "Additional host-to-address entries added to the container.", - "items": { - "type": "string" - }, - "type": "array" - }, - "health": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Readiness check used to gate rolling replacement.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "exec": { - "description": "Health command as a shell string or direct argument list." - }, - "http": { - "description": "HTTP path probed inside the container. Expects a path beginning with /.", - "examples": [ - "/healthz" - ], - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "interval": { - "default": "5s", - "description": "Delay between container health probes, at most 7d. Always written into the generated healthcheck, so the rollout's drain budget is computed from the value the container actually runs with. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "2s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "port": { - "description": "Container port probed by HTTP or TCP health checks.", - "examples": [ - 8080 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "retries": { - "default": 3, - "description": "Consecutive failed probes before the container is unhealthy. A draining container leaves rotation after this many probes, so it sets how long a rolling deploy waits for each replica.", - "examples": [ - 3 - ], - "type": "integer" - }, - "start_period": { - "default": "30s", - "description": "Startup grace period before failed probes count, at most 7d. Always written into the generated healthcheck, so writing down a fast probe interval does not call a booting container unhealthy. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "tcp": { - "default": false, - "description": "Probe the configured port by opening a TCP connection.", - "type": "boolean" - }, - "within": { - "description": "Maximum time a rollout waits for readiness, at most 7d. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "120s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Readiness check used to gate rolling replacement. Also accepts an HTTP health path." - }, - "hostname": { - "description": "Hostname assigned inside the workload container.", - "type": "string" - }, - "image": { - "anyOf": [ - { - "description": "Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - }, - { - "additionalProperties": false, - "description": "Container image source, written as a reference string or an object.", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "pull": { - "default": "missing", - "description": "When to fetch the image from the registry: missing fetches only what the host does not already hold, always fetches every release, never fetches at all and fails on a missing image.", - "enum": [ - "always", - "missing", - "never" - ], - "type": "string" - }, - "reference": { - "description": "Complete container image reference, optionally tagged or digest-pinned. Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Container image source, written as a reference string or an object. Also accepts an image reference." - }, - "init": { - "description": "Run a minimal init process as PID 1 inside the container.", - "type": "boolean" - }, - "inputs": { - "additionalProperties": { - "additionalProperties": false, - "oneOf": [ - { - "required": [ - "enum" - ] - }, - { - "required": [ - "pattern" - ] - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "default": { - "description": "Value used by a timer firing and by an operator run that does not override it. Must satisfy the input's own constraint.", - "type": "string" - }, - "description": { - "description": "What the input controls.", - "type": "string" - }, - "enum": { - "description": "Accepted values.", - "examples": [ - [ - "catalog" - ] - ], - "items": { - "type": "string" - }, - "type": "array" - }, - "pattern": { - "description": "Regular expression the whole value must match.", - "examples": [ - "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" - ], - "type": "string" - } - }, - "required": [ - "default" - ], - "type": "object" - }, - "description": "Declared parameters of a scheduled job, exposed as environment variables. Names are upper-case identifiers; each declares exactly one of enum or pattern and a default. A timer firing uses the defaults; ob job run may override them.", - "propertyNames": { - "pattern": "^[A-Z][A-Z0-9_]*$" - }, - "type": "object" - }, - "labels": { - "additionalProperties": {}, - "description": "Additional container labels outside namespaces reserved by Onebox and the proxy.", - "type": "object" - }, - "logging": { - "additionalProperties": false, - "description": "Container logging driver and driver-specific options.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "driver": { - "description": "Container runtime logging driver. Expects a log driver name such as local, json-file or an org/plugin:tag.", - "examples": [ - "local" - ], - "pattern": "^[a-z0-9][a-z0-9_.-]*(/[a-z0-9][a-z0-9_.-]*)?(:[A-Za-z0-9_.-]+)?$", - "type": "string" - }, - "options": { - "additionalProperties": {}, - "description": "Driver-specific logging options passed to the container runtime.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9_.-]*$" - }, - "type": "object" - } - }, - "type": "object" - }, - "needs": { - "description": "Workload or supporting-service prerequisites and optional connection-variable mappings.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "condition": { - "description": "Prerequisite condition: started, healthy, or completed.", - "enum": [ - "started", - "healthy", - "completed" - ], - "type": "string" - }, - "env": { - "additionalProperties": { - "type": "string" - }, - "description": "Maps application environment-variable names to service connection parts such as host, port, user, password, database, or url.", - "type": "object" - }, - "name": { - "description": "Name of a workload or supporting service that must start first. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Also accepts the name of a prerequisite." - }, - "type": "array" - }, - "operator_run": { - "description": "Whether an operator may invoke this job outside deployment: allowed or disabled. Defaults to allowed for phase none and disabled otherwise.", - "enum": [ - "allowed", - "disabled" - ], - "type": "string" - }, - "persistence": { - "additionalProperties": false, - "description": "Declares whether this workload holds data that must outlive releases.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "mode": { - "default": "durable", - "description": "Data lifetime: durable, ephemeral, or external.", - "enum": [ - "durable", - "ephemeral", - "external" - ], - "type": "string" - } - }, - "type": "object" - }, - "port": { - "description": "Default container port used by HTTP health checks.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "published_ports": { - "description": "Host ports published outside the proxy. They bind to loopback by default. A rolling workload cannot publish one, because two replicas cannot hold the same host port during a roll: set strategy: recreate, or route through the proxy instead.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "bind": { - "default": "127.0.0.1", - "description": "Host address on which the published port listens.", - "type": "string" - }, - "container": { - "description": "Port receiving traffic inside the container.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "host": { - "description": "Port exposed on the host.", - "examples": [ - 8080 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "protocol": { - "default": "tcp", - "description": "Published transport protocol: tcp or udp.", - "enum": [ - "tcp", - "udp" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "replicas": { - "default": 1, - "description": "Desired number of long-running workload containers.", - "examples": [ - 2 - ], - "minimum": 1, - "type": "integer" - }, - "resources": { - "additionalProperties": false, - "description": "Container memory and CPU limits.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cpus": { - "description": "Container CPU limit expressed as a positive decimal count. Expects a number of CPUs such as 0.5 or 2.", - "examples": [ - "0.5" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?$", - "type": "string" - }, - "memory": { - "description": "Container memory limit. Expects a size such as 512MB or 1.5GB.", - "examples": [ - "512MB" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?(B|KB|MB|GB|TB)$", - "type": "string" - } - }, - "type": "object" - }, - "role": { - "description": "Lifecycle role: application, worker, daemon, or job.", - "enum": [ - "application", - "worker", - "daemon", - "job" - ], - "examples": [ - "application" - ], - "type": "string" - }, - "routes": { - "description": "Ingress routes exposed by this workload.", - "items": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "hostname": { - "const": "*" - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - }, - "tls": { - "enum": [ - "none", - "passthrough" - ] - } - }, - "required": [ - "protocol", - "tls" - ] - } - }, - { - "if": { - "properties": { - "hostname": { - "pattern": "^\\*\\." - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "http" - } - } - } - }, - { - "if": { - "properties": { - "tls": { - "const": "passthrough" - } - }, - "required": [ - "tls" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - } - }, - "required": [ - "protocol" - ] - } - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, - "hostname": { - "anyOf": [ - { - "maxLength": 253, - "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" - }, - { - "const": "*" - } - ], - "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", - "examples": [ - "shop.example.com" - ], - "type": "string" - }, - "middlewares": { - "description": "Ordered provider-qualified middleware references applied to this route.", - "items": { - "description": "Expects a provider-qualified name such as secure-headers@file.", - "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*@[a-z][a-z0-9-]*$", - "type": "string" - }, - "type": "array" - }, - "path": { - "default": "/", - "description": "URL path prefix matched by an HTTP route. Expects a path beginning with /.", - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "port": { - "description": "Container port receiving routed traffic.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "protocol": { - "default": "http", - "description": "Routing protocol: http or tcp.", - "enum": [ - "http", - "tcp" - ], - "type": "string" - }, - "scheme": { - "default": "http", - "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", - "enum": [ - "http", - "https", - "h2c" - ], - "type": "string" - }, - "tls": { - "default": "terminate", - "description": "TLS handling: terminate, passthrough, or none.", - "enum": [ - "terminate", - "passthrough", - "none" - ], - "type": "string" - } - }, - "required": [ - "hostname" - ], - "type": "object" - }, - "type": "array" - }, - "schedule": { - "additionalProperties": false, - "description": "Host-resident recurring schedule and run policy for a job, independent of its deployment phase and operator-run policy.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "catch_up": { - "default": true, - "description": "Run once after the host returns if an elapsed schedule was missed while it was offline.", - "type": "boolean" - }, - "cron": { - "description": "Five-field cron schedule translated to a host timer. Expects five cron fields.", - "examples": [ - "0 2 * * *" - ], - "pattern": "^[-0-9*/,A-Za-z ]+$", - "type": "string" - }, - "deploy_lock": { - "default": "exclusive", - "description": "Deployment coordination policy: exclusive blocks application operations for the full run; pinned leases the immutable starting release and permits only deployments without data-changing jobs or untyped hooks.", - "enum": [ - "exclusive", - "pinned" - ], - "examples": [ - "pinned" - ], - "type": "string" - }, - "notify": { - "default": [ - "failure", - "timeout" - ], - "description": "Run outcomes that send the configured notifications: success, failure, timeout, skipped.", - "items": { - "enum": [ - "success", - "failure", - "timeout", - "skipped" - ], - "type": "string" - }, - "type": "array" - }, - "retry": { - "additionalProperties": false, - "description": "Bounded retry inside one timer firing. Attempts run under the same locks and the same timeout; a timeout ends the run.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "attempts": { - "default": 1, - "description": "Total attempts including the first, 1 to 10.", - "examples": [ - 3 - ], - "maximum": 10, - "minimum": 1, - "type": "integer" - }, - "backoff": { - "default": "30s", - "description": "Sleep before the second attempt; it doubles after each failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "1m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "max_backoff": { - "default": "10m", - "description": "Upper bound for the doubling sleep. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - }, - "shutdown_grace": { - "default": "30s", - "description": "Time allowed for graceful container shutdown after the run deadline before Onebox forces removal. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "45s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "timeout": { - "default": "1h", - "description": "Maximum wall time for one scheduled run before systemd terminates it and records failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "timezone": { - "default": "UTC", - "description": "IANA timezone used to interpret the cron schedule. Expects an IANA zone name such as UTC or Europe/Berlin.", - "examples": [ - "Europe/Berlin" - ], - "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$", - "type": "string" - } - }, - "type": "object" - }, - "stdin_open": { - "description": "Keep standard input open for the container.", - "type": "boolean" - }, - "strategy": { - "description": "Replacement strategy for a changed or uncertain workload. An unchanged healthy workload is retained automatically. Defaults to rolling only for an application workload with health; all other workloads default to recreate.", - "enum": [ - "rolling", - "recreate" - ], - "type": "string" - }, - "tty": { - "description": "Allocate a pseudo-TTY for the container.", - "type": "boolean" - }, - "user": { - "description": "User or UID used to run the container process.", - "type": "string" - }, - "volumes": { - "description": "Managed named volumes or bind mounts. Relative bind sources are read-only release content; absolute sources are external host state.", - "items": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "source": { - "pattern": "^[^/]" - } - }, - "required": [ - "source" - ] - }, - "then": { - "properties": { - "mode": { - "const": "ro" - } - }, - "required": [ - "mode" - ] - } - } - ], - "anyOf": [ - { - "required": [ - "name", - "path" - ] - }, - { - "required": [ - "source", - "path" - ] - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "mode": { - "default": "rw", - "description": "Mount access mode: rw or ro. A relative bind source requires ro.", - "enum": [ - "rw", - "ro" - ], - "type": "string" - }, - "name": { - "description": "Stable logical name of a Onebox-managed volume. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "examples": [ - "data" - ], - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "path": { - "description": "Absolute container path where the volume or bind mount is attached. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/var/lib/app" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "source": { - "description": "Bind mount source. An absolute path is external host state that outlives releases. A dot-prefixed repository path is read-only release content, kept for as long as a container still mounts it. Expects an absolute host path or a dot-prefixed path inside the repository, with no colon, control character or shell metacharacter.", - "examples": [ - "./config" - ], - "not": { - "pattern": "(^|/)\\.\\.(/|$)" - }, - "pattern": "^(/[^\\x00-\\x1f'\"$`\\\\:]*|\\.(?:/[^\\x00-\\x1f'\"$`\\\\:]*)?)$", - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "working_dir": { - "description": "Absolute working directory for the container process. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/app" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - } - }, - "type": "object" - }, - "description": "Application containers, workers, daemons, and jobs managed as releases.", - "type": "object" - } - }, - "required": [ - "api_version", - "environments" - ], - "title": "Onebox project (onebox.run/v2)", - "type": "object" -} diff --git a/e2e/apps/README.md b/e2e/apps/README.md index 105b2638..9d3ccf4a 100644 --- a/e2e/apps/README.md +++ b/e2e/apps/README.md @@ -1,6 +1,6 @@ # Deployable application fixtures -Self-contained `onebox.run/v2` projects for real open-source applications, +Self-contained `onebox.run/v1` projects for real open-source applications, chosen for the shape people normally build rather than for being exotic. Each declares everything it needs, so it renders and runs without a Compose reference. diff --git a/e2e/apps/authentik.yml b/e2e/apps/authentik.yml index 49bb9bdb..8423c594 100644 --- a/e2e/apps/authentik.yml +++ b/e2e/apps/authentik.yml @@ -2,7 +2,7 @@ # application's image — none of which any deployed fixture had, and the bind # mount in particular is the one a converted Compose file almost always brings # with it. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: authentik environments: production: {server: root@TARGET} diff --git a/e2e/apps/ghost.yml b/e2e/apps/ghost.yml index 4c3bfd0a..f96469d4 100644 --- a/e2e/apps/ghost.yml +++ b/e2e/apps/ghost.yml @@ -1,7 +1,7 @@ # Ghost on managed MySQL. The point of this fixture is the driver: mysql is in # the catalogue and had never run on a host, so the credential generation, the # connection file and the health check were all unproven for it. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: ghost environments: production: {server: root@TARGET} diff --git a/e2e/apps/gitea.yml b/e2e/apps/gitea.yml index ca6427af..27e89b6a 100644 --- a/e2e/apps/gitea.yml +++ b/e2e/apps/gitea.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: gitea environments: production: {server: root@TARGET} diff --git a/e2e/apps/immich.yml b/e2e/apps/immich.yml index 29e7105a..f6d13d14 100644 --- a/e2e/apps/immich.yml +++ b/e2e/apps/immich.yml @@ -1,7 +1,7 @@ # Immich. Its database is Postgres with the pgvector extension, which is not # the image the managed driver runs — so it is a daemon the user owns, which is # exactly the boundary the contract draws. Also the heaviest images in the set. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: immich environments: production: {server: root@TARGET} diff --git a/e2e/apps/n8n.yml b/e2e/apps/n8n.yml index f45f5ecf..a6901d8e 100644 --- a/e2e/apps/n8n.yml +++ b/e2e/apps/n8n.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: n8n environments: production: {server: root@TARGET} diff --git a/e2e/apps/paperless.yml b/e2e/apps/paperless.yml index fe00581e..003afd99 100644 --- a/e2e/apps/paperless.yml +++ b/e2e/apps/paperless.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: paperless environments: production: {server: root@TARGET} diff --git a/e2e/apps/penpot.yml b/e2e/apps/penpot.yml index 6c1ee73e..1feb26d2 100644 --- a/e2e/apps/penpot.yml +++ b/e2e/apps/penpot.yml @@ -1,7 +1,7 @@ # Penpot. Two routes on one workload's host — the frontend serves the app and # proxies /api to the backend — which is the multi-route shape that only ever # existed in a synthetic fixture until now. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: penpot environments: production: {server: root@TARGET} diff --git a/e2e/apps/rocketchat.yml b/e2e/apps/rocketchat.yml index 83e7bd12..260e5085 100644 --- a/e2e/apps/rocketchat.yml +++ b/e2e/apps/rocketchat.yml @@ -10,7 +10,7 @@ # Kept as a rendering and validation case, and as the thing to re-run if the # driver ever configures a replica set. An application that needs one today # wants a daemon workload it owns. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: rocketchat environments: production: {server: root@TARGET} diff --git a/e2e/apps/umami.yml b/e2e/apps/umami.yml index 8960402a..1c3c44e1 100644 --- a/e2e/apps/umami.yml +++ b/e2e/apps/umami.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: umami environments: production: {server: root@TARGET} diff --git a/e2e/apps/uptime-kuma.yml b/e2e/apps/uptime-kuma.yml index 53ea0498..885d68db 100644 --- a/e2e/apps/uptime-kuma.yml +++ b/e2e/apps/uptime-kuma.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: uptime-kuma environments: production: {server: root@TARGET} diff --git a/e2e/apps/vaultwarden.yml b/e2e/apps/vaultwarden.yml index 85dce82b..3edf7fe1 100644 --- a/e2e/apps/vaultwarden.yml +++ b/e2e/apps/vaultwarden.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: vaultwarden environments: production: {server: root@TARGET} diff --git a/e2e/destroy_test.go b/e2e/destroy_test.go index de36bc62..c9dff16f 100644 --- a/e2e/destroy_test.go +++ b/e2e/destroy_test.go @@ -27,7 +27,7 @@ func TestDestroyUsesReleaseRecordedInterpolationEnvironment(t *testing.T) { releaseID := "20260821-120000-legacy" volume := application + "_legacy_data" - currentBody := fmt.Sprintf(`api_version: onebox.run/v2 + currentBody := fmt.Sprintf(`api_version: onebox.run/v1 app: %s base_path: %q environments: diff --git a/e2e/network_ownership_test.go b/e2e/network_ownership_test.go index 264949ed..b791a8f7 100644 --- a/e2e/network_ownership_test.go +++ b/e2e/network_ownership_test.go @@ -24,7 +24,7 @@ func TestApplicationNetworkOwnershipAndExternalLifecycle(t *testing.T) { application := fmt.Sprintf("obnet%d", os.Getpid()) network := application + "_default" - projectBody := fmt.Sprintf(`api_version: onebox.run/v2 + projectBody := fmt.Sprintf(`api_version: onebox.run/v1 app: %s environments: production: {server: root@localhost} diff --git a/e2e/server_execution_test.go b/e2e/server_execution_test.go index ed8094e6..1d0e3c4e 100644 --- a/e2e/server_execution_test.go +++ b/e2e/server_execution_test.go @@ -27,7 +27,7 @@ func TestServerDurableExecutions(t *testing.T) { defer cancel() _, _ = s.output(ctx, "systemctl disable --now "+unit+".timer >/dev/null 2>&1; systemctl stop "+unit+".service >/dev/null 2>&1; docker rm -f "+name+"-refresh-1 >/dev/null 2>&1; rm -f /etc/systemd/system/"+unit+".*; systemctl daemon-reload; rm -rf "+base) }) - project := fmt.Sprintf(`api_version: onebox.run/v2 + project := fmt.Sprintf(`api_version: onebox.run/v1 app: %s base_path: %s environments: {production: {server: %s}} diff --git a/e2e/testdata/app/ob.yml b/e2e/testdata/app/ob.yml index f14928c5..a1d17b80 100644 --- a/e2e/testdata/app/ob.yml +++ b/e2e/testdata/app/ob.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: obe2e environments: production: { server: local } # e2e uses the local transport; the value is unused diff --git a/e2e/testdata/postgres/ob.yml.tmpl b/e2e/testdata/postgres/ob.yml.tmpl index 9de314c2..5cd76811 100644 --- a/e2e/testdata/postgres/ob.yml.tmpl +++ b/e2e/testdata/postgres/ob.yml.tmpl @@ -4,7 +4,7 @@ # Rendered rather than checked in whole, because two values are only known once # the guest is up: the address ob connects to, and the endpoint the object # store is reachable at from inside the container. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: observer environments: production: diff --git a/e2e/testdata/worker/ob-broken.yml b/e2e/testdata/worker/ob-broken.yml index 9205205c..370aa171 100644 --- a/e2e/testdata/worker/ob-broken.yml +++ b/e2e/testdata/worker/ob-broken.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: obworker environments: { production: { server: local } } workloads: diff --git a/e2e/testdata/worker/ob.yml b/e2e/testdata/worker/ob.yml index abb32764..3b8df8c4 100644 --- a/e2e/testdata/worker/ob.yml +++ b/e2e/testdata/worker/ob.yml @@ -1,4 +1,4 @@ -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: obworker environments: { production: { server: local } } workloads: diff --git a/internal/app/backup_schema_test.go b/internal/app/backup_schema_test.go index 57b876fc..806eaf82 100644 --- a/internal/app/backup_schema_test.go +++ b/internal/app/backup_schema_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -const validBackupProject = `api_version: onebox.run/v2 +const validBackupProject = `api_version: onebox.run/v1 app: shop environments: production: @@ -66,7 +66,7 @@ func TestBackupIntentLoadsAndDefaultsToExactSchedules(t *testing.T) { // The refusal belongs at the point the policy is written, so this is now the // same rejection every other unqualified driver gets. func TestMinIOBackupIntentIsRefusedUntilItsContractRuns(t *testing.T) { - project := `api_version: onebox.run/v2 + project := `api_version: onebox.run/v1 app: shop environments: {production: {server: deploy@app.example.net}} workloads: {web: {image: nginx:1}} @@ -100,7 +100,7 @@ func TestReplicationIntentIsRejected(t *testing.T) { } func TestRunnableUnqualifiedDriverRejectsBackupWithoutFallback(t *testing.T) { - if _, err := LoadBytes([]byte(`api_version: onebox.run/v2 + if _, err := LoadBytes([]byte(`api_version: onebox.run/v1 app: shop environments: {production: {server: deploy@app.example.net}} workloads: {web: {image: nginx:1}} diff --git a/internal/app/canonical_test.go b/internal/app/canonical_test.go index 96251e44..e220d11b 100644 --- a/internal/app/canonical_test.go +++ b/internal/app/canonical_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -const canonicalProject = `api_version: onebox.run/v2 +const canonicalProject = `api_version: onebox.run/v1 app: ledger environments: production: {server: root@1.2.3.4} @@ -216,7 +216,7 @@ func TestCanonicalFactsRejectUnsafeObservedValuesWithoutReflectingThem(t *testin // silently absent, and the canonical form — the thing people read to find out // what Onebox understood — did not show it either. func TestEveryDefaultAppearsAsDerived(t *testing.T) { - spec, err := LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := LoadBytes([]byte(`api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: diff --git a/internal/app/compose_test.go b/internal/app/compose_test.go index 1cee2347..fcfa97f8 100644 --- a/internal/app/compose_test.go +++ b/internal/app/compose_test.go @@ -132,7 +132,7 @@ func TestPathEscapeRefused(t *testing.T) { // TestComposeRefRendersEndToEnd puts the merge through generation. func TestComposeRefRendersEndToEnd(t *testing.T) { - y := `api_version: onebox.run/v2 + y := `api_version: onebox.run/v1 app: ledger environments: production: {server: root@1.2.3.4} @@ -227,7 +227,7 @@ func TestADeclaredHealthCheckReachesAReferencedService(t *testing.T) { t.Fatal(err) } path := filepath.Join(dir, "ob.yml") - if err := os.WriteFile(path, []byte(`api_version: onebox.run/v2 + if err := os.WriteFile(path, []byte(`api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} diff --git a/internal/app/constraints.go b/internal/app/constraints.go index fb3e63ee..3cfcf52f 100644 --- a/internal/app/constraints.go +++ b/internal/app/constraints.go @@ -116,11 +116,6 @@ var ( gRouteHostname = grammar{"route hostname", regexp.MustCompile(`^(\*\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$`), "a lower-case ASCII or IDNA A-label hostname, optionally prefixed by the complete wildcard label *."} - // v1 exact route hosts were deliberately permissive. Immutable v1 release - // snapshots must retain that grammar so a v2 runner can still roll them back - // or finish their lifecycle; new authored projects never use it. - gLegacyRouteHost = grammar{"route host", regexp.MustCompile("^[^\\x00-\\x1f\\x7f`*]+$"), - "an exact host with no wildcard, control character or backtick"} gCalVer = grammar{"version", buildinfo.ReleaseVersionPattern, "a CalVer release such as v2026.8.0"} diff --git a/internal/app/contract_shapes_test.go b/internal/app/contract_shapes_test.go index 2eaea824..5c5ae342 100644 --- a/internal/app/contract_shapes_test.go +++ b/internal/app/contract_shapes_test.go @@ -39,7 +39,7 @@ func canonicalOf(t *testing.T, body string) string { return string(out) } -const shapeHead = "api_version: onebox.run/v2\napp: shop\n" +const shapeHead = "api_version: onebox.run/v1\napp: shop\n" // 3.4 — a scalar shorthand and its object form are the same project. // diff --git a/internal/app/eject_test.go b/internal/app/eject_test.go index 9809db35..b0b0edba 100644 --- a/internal/app/eject_test.go +++ b/internal/app/eject_test.go @@ -8,7 +8,7 @@ import ( ) const ejectProject = `# Ledger's production contract. -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: ledger environments: @@ -190,7 +190,7 @@ func TestEjectCarriesTheAuthorsNote(t *testing.T) { // shaped by the file. Leaving a health check or a volume in the project would // let someone edit it, see no effect, and get no error. func TestEjectRemovesWhatTheComposeFileNowOwns(t *testing.T) { - dir, _ := ejectInto(t, `api_version: onebox.run/v2 + dir, _ := ejectInto(t, `api_version: onebox.run/v1 app: ledger environments: {production: {server: root@1.2.3.4}} workloads: @@ -224,7 +224,7 @@ workloads: func TestEjectDefaultAvoidsAReferencedFile(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "compose.yaml"), []byte("services:\n db: {image: postgres}\n"), 0o600) - os.WriteFile(filepath.Join(dir, "ob.yml"), []byte(`api_version: onebox.run/v2 + os.WriteFile(filepath.Join(dir, "ob.yml"), []byte(`api_version: onebox.run/v1 app: ledger environments: {production: {server: root@1.2.3.4}} workloads: @@ -260,7 +260,7 @@ workloads: func TestEjectAfterAnInterruptionCompletes(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "ob.yml") - body := `api_version: onebox.run/v2 + body := `api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} diff --git a/internal/app/ejection_contract_test.go b/internal/app/ejection_contract_test.go index bdabfb6d..a55c1b70 100644 --- a/internal/app/ejection_contract_test.go +++ b/internal/app/ejection_contract_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -const ejectContractProject = `api_version: onebox.run/v2 +const ejectContractProject = `api_version: onebox.run/v1 app: shop environments: production: diff --git a/internal/app/environment_model_test.go b/internal/app/environment_model_test.go index 116859da..3238ff09 100644 --- a/internal/app/environment_model_test.go +++ b/internal/app/environment_model_test.go @@ -55,7 +55,7 @@ func listFor(t *testing.T, r *Resolved, workload string) []string { return out } -const envModelBody = `api_version: onebox.run/v2 +const envModelBody = `api_version: onebox.run/v1 app: shop environments: production: {server: root@203.0.113.10} @@ -138,7 +138,7 @@ func TestTwoEntriesNeverShareAStagedFile(t *testing.T) { // The withdrawn block is refused with direction, not as an unknown field. func TestTheWithdrawnSecretsBlockIsRefusedWithDirection(t *testing.T) { - _, err := Load(envModelProject(t, `api_version: onebox.run/v2 + _, err := Load(envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} image: nginx @@ -163,7 +163,7 @@ secrets: {production: s.yaml} // An authored value may not claim a name a connection supplies. func TestAuthoredValuesCannotClaimAConnectionVariable(t *testing.T) { - _, err := Load(envModelProject(t, `api_version: onebox.run/v2 + _, err := Load(envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: @@ -189,7 +189,7 @@ services: // A compose-sourced application receives what an image-sourced one receives, // and ejecting then generating does not duplicate the projection. func TestComposeSourcedWorkloadsAreNotASpecialCase(t *testing.T) { - path := envModelProject(t, `api_version: onebox.run/v2 + path := envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} runtime: @@ -243,7 +243,7 @@ workloads: // rolling release waited out its entire budget and then reported the container // unhealthy, naming the container and saying nothing about the port. func TestAnHTTPProbeInheritsTheRoutedPort(t *testing.T) { - r := resolvedFor(t, envModelProject(t, `api_version: onebox.run/v2 + r := resolvedFor(t, envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} image: nginx @@ -270,7 +270,7 @@ health: /healthz // a contract treating "how it is stored" as "who may see it" would let the // commoner form leak. func TestNoEntryValueReachesAnArtifact(t *testing.T) { - path := envModelProject(t, `api_version: onebox.run/v2 + path := envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} runtime: @@ -310,7 +310,7 @@ routes: // rolling release waits out in full before reporting the container unhealthy // without naming a port. func TestAProbeWithNoPortIsRefused(t *testing.T) { - _, err := Load(envModelProject(t, `api_version: onebox.run/v2 + _, err := Load(envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: @@ -349,7 +349,7 @@ func composeServiceEnvFiles(t *testing.T, runtime []byte, service string) []stri // adds. Both halves were unguarded — deleting the projection outright left the // suite green. func TestTheProjectionAppendsAndPreservesOrder(t *testing.T) { - path := envModelProject(t, `api_version: onebox.run/v2 + path := envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} runtime: @@ -387,7 +387,7 @@ workloads: // cannot be shadowed by one. Emitting them in the other order passed every // test. func TestConnectionFilesComeAfterDeclaredEntries(t *testing.T) { - path := envModelProject(t, `api_version: onebox.run/v2 + path := envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} runtime: @@ -420,7 +420,7 @@ services: // half was tested; this half is a scenario stated twice in the contract and had // no test — making the check unconditionally return nil passed everything. func TestAReferencedServiceCannotClaimAConnectionVariable(t *testing.T) { - _, err := resolvedForErr(t, envModelProject(t, `api_version: onebox.run/v2 + _, err := resolvedForErr(t, envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} workloads: @@ -469,7 +469,7 @@ func resolvedForErr(t *testing.T, path string) ([]byte, error) { // asked for interpolation. Stopping a correct project from loading is a worse // failure than the one it would prevent. func TestAnEncryptedEntryDoesNotBlockAProjectThatNeedsNoInterpolation(t *testing.T) { - path := envModelProject(t, `api_version: onebox.run/v2 + path := envModelProject(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@203.0.113.10}} runtime: @@ -547,7 +547,7 @@ func TestAnOverrideDeclaringNoneIsPreserved(t *testing.T) { // after the release is staged and the old one is coming down. The name is in // the document; there is no reason to find out there. func TestAnEntryNamingAMissingFileIsRefused(t *testing.T) { - body := `api_version: onebox.run/v2 + body := `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} runtime: diff --git a/internal/app/external_schema_test.go b/internal/app/external_schema_test.go index b08bd29d..f5b51203 100644 --- a/internal/app/external_schema_test.go +++ b/internal/app/external_schema_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -const validExternalServiceProject = `api_version: onebox.run/v2 +const validExternalServiceProject = `api_version: onebox.run/v1 app: shop environments: {production: {server: deploy@app.example.net}} workloads: @@ -37,7 +37,7 @@ func TestExternalServiceFixtures(t *testing.T) { }, { name: "external_service_ambiguous_owner", - yaml: `api_version: onebox.run/v2 + yaml: `api_version: onebox.run/v1 app: shop environments: {production: {server: deploy@app.example.net}} workloads: {web: {image: nginx:1}} @@ -54,7 +54,7 @@ external_services: }, { name: "external_service_lifecycle_field_refused", - yaml: `api_version: onebox.run/v2 + yaml: `api_version: onebox.run/v1 app: shop environments: {production: {server: deploy@app.example.net}} workloads: {web: {image: nginx:1}} diff --git a/internal/app/generate_test.go b/internal/app/generate_test.go index 83dfa717..b5d0a726 100644 --- a/internal/app/generate_test.go +++ b/internal/app/generate_test.go @@ -11,7 +11,7 @@ import ( // A decent-size project of the shape people actually build: a web application, // a background worker, a migration job, and a database they still author. -const appFixture = `api_version: onebox.run/v2 +const appFixture = `api_version: onebox.run/v1 app: ledger environments: production: {server: root@1.2.3.4} @@ -86,7 +86,7 @@ func TestRenderIsDeterministic(t *testing.T) { } func TestWorkloadRevisionIsReleaseIndependentAndRuntimeSensitive(t *testing.T) { - project := `api_version: onebox.run/v2 + project := `api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -362,7 +362,7 @@ func TestHasTerminatingTLSDistinguishesPassthrough(t *testing.T) { } func TestWildcardRouteRendersSafeHostRegexpAndDNSResolver(t *testing.T) { - project := `api_version: onebox.run/v2 + project := `api_version: onebox.run/v1 app: preview environments: {production: {server: root@example.com}} workloads: @@ -440,7 +440,7 @@ func TestEveryDraftRenders(t *testing.T) { // showed standing between two thirds of services and the declaration. Each // carries no Onebox semantics: it is declared, and it appears. func TestPassthroughFields(t *testing.T) { - y := `api_version: onebox.run/v2 + y := `api_version: onebox.run/v1 app: ledger environments: production: {server: root@1.2.3.4} @@ -482,7 +482,7 @@ workloads: // generates into are reserved, so a user label can never silently win. func TestUserLabelsCannotClaimOneboxNamespaces(t *testing.T) { for _, bad := range []string{"ob.app", "traefik.enable"} { - y := `api_version: onebox.run/v2 + y := `api_version: onebox.run/v1 app: ledger environments: {production: {server: h}} workloads: {web: {role: application, image: nginx, labels: {"` + bad + `": x}}} @@ -519,7 +519,7 @@ func TestVolumeNamesArePinned(t *testing.T) { // workload that can never be released, so the exec form must reach the runtime // as CMD rather than CMD-SHELL. func TestExecListHealthRunsWithoutAShell(t *testing.T) { - out := render(t, `api_version: onebox.run/v2 + out := render(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: @@ -540,7 +540,7 @@ workloads: // The string form still runs through a shell, which is what makes `pg_isready // -U x && test -f /ready` work. func TestExecStringHealthKeepsItsShell(t *testing.T) { - out := render(t, `api_version: onebox.run/v2 + out := render(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: @@ -561,7 +561,7 @@ func TestShellHealthChecksCarryTheDrainGuard(t *testing.T) { `health: {tcp: true, port: 5432}`, `health: {exec: "test -f /ready"}`, } { - out := string(render(t, `api_version: onebox.run/v2 + out := string(render(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: @@ -580,7 +580,7 @@ workloads: // command and stays unquoted; a path is not, and must be one argument. func TestHTTPHealthPathIsQuotedInsideItsShellCheck(t *testing.T) { const injected = "/healthz;id>/tmp/ob-owned" - out := string(render(t, `api_version: onebox.run/v2 + out := string(render(t, `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: diff --git a/internal/app/health_timing_test.go b/internal/app/health_timing_test.go index 0acd172e..6ce62239 100644 --- a/internal/app/health_timing_test.go +++ b/internal/app/health_timing_test.go @@ -111,7 +111,7 @@ func TestReadyBudgetCoversAtLeastOneFlipCycle(t *testing.T) { // it negative, which expires instantly — the failure the budget exists to // prevent, reached by a route validation could have closed. func TestAbsurdRetriesIsRejected(t *testing.T) { - _, err := LoadBytes([]byte("api_version: onebox.run/v2\napp: ledger\n"+ + _, err := LoadBytes([]byte("api_version: onebox.run/v1\napp: ledger\n"+ "environments: {production: {server: root@10.0.0.1}}\n"+ "image: nginx\nroutes: [{hostname: d.example.com, port: 8080}]\n"+ "health: {http: /healthz, retries: 100000000000}\n"), "ob.yml") @@ -134,7 +134,7 @@ func TestAbsurdHealthDurationsAreRejected(t *testing.T) { "within": "{http: /healthz, within: 100000d}", } { t.Run(name, func(t *testing.T) { - _, err := LoadBytes([]byte("api_version: onebox.run/v2\napp: ledger\n"+ + _, err := LoadBytes([]byte("api_version: onebox.run/v1\napp: ledger\n"+ "environments: {production: {server: root@10.0.0.1}}\n"+ "image: nginx\nroutes: [{hostname: d.example.com, port: 8080}]\n"+ "health: "+health+"\n"), "ob.yml") @@ -206,7 +206,7 @@ func TestParseDurationRejectsOverflowingDayCounts(t *testing.T) { // A day count that wraps int64 must be rejected by validation too, not merely // by the parser: the two together are what make the bound mean something. func TestOverflowingDayCountIsRejectedAtLoad(t *testing.T) { - _, err := LoadBytes([]byte("api_version: onebox.run/v2\napp: ledger\n"+ + _, err := LoadBytes([]byte("api_version: onebox.run/v1\napp: ledger\n"+ "environments: {production: {server: root@10.0.0.1}}\n"+ "image: nginx\nroutes: [{hostname: d.example.com, port: 8080}]\n"+ "health: {http: /healthz, interval: 1000000d}\n"), "ob.yml") @@ -224,7 +224,7 @@ func TestAbsurdDrainDurationsAreRejected(t *testing.T) { "grace": "{grace: 100000d}", } { t.Run(name, func(t *testing.T) { - _, err := LoadBytes([]byte("api_version: onebox.run/v2\napp: ledger\n"+ + _, err := LoadBytes([]byte("api_version: onebox.run/v1\napp: ledger\n"+ "environments: {production: {server: root@10.0.0.1}}\n"+ "workloads: {web: {image: nginx, routes: [{hostname: d.example.com, port: 8080}], drain: "+drain+"}}\n"), "ob.yml") if err == nil { diff --git a/internal/app/jsonschema.go b/internal/app/jsonschema.go index 4474c5dc..6158baf3 100644 --- a/internal/app/jsonschema.go +++ b/internal/app/jsonschema.go @@ -22,7 +22,7 @@ import ( // SchemaID is both the schema identity and its stable, publicly retrievable // location. The main-branch path stays fixed across Onebox releases. -const SchemaID = "https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json" +const SchemaID = "https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v1.schema.json" // JSONSchema is the published contract, ready to write. func JSONSchema() ([]byte, error) { @@ -35,7 +35,7 @@ func JSONSchema() ([]byte, error) { } doc["$schema"] = "https://json-schema.org/draft/2020-12/schema" doc["$id"] = SchemaID - doc["title"] = "Onebox project (onebox.run/v2)" + doc["title"] = "Onebox project (onebox.run/v1)" doc["description"] = "One application, its workloads, the services it needs, and how a release rolls out." // The constraints the loader enforces, so the schema refuses what the diff --git a/internal/app/jsonschema_test.go b/internal/app/jsonschema_test.go index dc1501da..7d04b1ce 100644 --- a/internal/app/jsonschema_test.go +++ b/internal/app/jsonschema_test.go @@ -124,7 +124,7 @@ func TestPublishedSchemaAcceptsEveryRealProject(t *testing.T) { func TestPublishedSchemaRequiresExecutionStepIDAndCommand(t *testing.T) { schema := compiledSchema(t) for _, step := range []string{`{id: sync, command: [echo, ok]}`, `{command: [echo, ok]}`, `{id: sync}`, `{}`} { - y := "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads:\n sync:\n role: job\n image: busybox\n deployment_phase: none\n data_effect: none\n schedule: {cron: '0 * * * *'}\n execution:\n steps: [" + step + "]\n" + y := "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads:\n sync:\n role: job\n image: busybox\n deployment_phase: none\n data_effect: none\n schedule: {cron: '0 * * * *'}\n execution:\n steps: [" + step + "]\n" err := schema.Validate(asJSON(t, y)) valid := strings.Contains(step, "id:") && strings.Contains(step, "command:") if (err == nil) != valid { @@ -136,11 +136,11 @@ func TestPublishedSchemaRequiresExecutionStepIDAndCommand(t *testing.T) { func TestPublishedSchemaAcceptsAuthoredShorthand(t *testing.T) { schema := compiledSchema(t) for _, y := range []string{ - "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nimage: nginx\n", - "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nservices: {postgres: 17}\n", - "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx, volumes: [{name: data, path: /data}], needs: [db], command: run}}\n", - "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nhooks: {post_deploy: \"echo done\"}\n", - "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nx-note: anything\n", + "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nimage: nginx\n", + "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nservices: {postgres: 17}\n", + "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx, volumes: [{name: data, path: /data}], needs: [db], command: run}}\n", + "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nhooks: {post_deploy: \"echo done\"}\n", + "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nx-note: anything\n", } { if err := schema.Validate(asJSON(t, y)); err != nil { t.Errorf("authored shorthand rejected:\n%s\n%v", y, err) @@ -152,7 +152,7 @@ func TestPublishedSchemaAcceptsAuthoredShorthand(t *testing.T) { // completion and error support the schema exists to provide. func TestPublishedSchemaRefusesAnUndefinedField(t *testing.T) { schema := compiledSchema(t) - y := "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx, replicaz: 3}}\n" + y := "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx, replicaz: 3}}\n" if err := schema.Validate(asJSON(t, y)); err == nil { t.Error("the published schema accepted a field the contract does not define") } else if !strings.Contains(err.Error(), "replicaz") { @@ -162,7 +162,7 @@ func TestPublishedSchemaRefusesAnUndefinedField(t *testing.T) { func TestPublishedSchemaConstrainsProxyEntrypoints(t *testing.T) { schema := compiledSchema(t) - base := "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nproxy:\n entrypoints:\n" + base := "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: root@h}}\nworkloads: {w: {image: nginx}}\nproxy:\n entrypoints:\n" for _, tc := range []struct { name string @@ -267,13 +267,13 @@ func TestCheckedInSchemaMatchesGenerator(t *testing.T) { t.Fatal(err) } want := append(append([]byte(nil), body...), '\n') - path := filepath.Join("..", "..", "docs", "onebox.run-v2.schema.json") + path := filepath.Join("..", "..", "docs", "onebox.run-v1.schema.json") got, err := os.ReadFile(path) if err != nil { t.Fatalf("read published schema: %v", err) } if !bytes.Equal(got, want) { - t.Fatalf("%s is stale; regenerate it with `go run ./cmd/ob schema --out docs/onebox.run-v2.schema.json`", path) + t.Fatalf("%s is stale; regenerate it with `go run ./cmd/ob schema --out docs/onebox.run-v1.schema.json`", path) } } diff --git a/internal/app/jump_config_test.go b/internal/app/jump_config_test.go index 70ea75f2..4fe173a6 100644 --- a/internal/app/jump_config_test.go +++ b/internal/app/jump_config_test.go @@ -6,7 +6,7 @@ import ( ) func projectWithJump(jump string) string { - return "api_version: onebox.run/v2\napp: ledger\n" + + return "api_version: onebox.run/v1\napp: ledger\n" + "environments: {production: {server: root@10.20.0.10, jump: " + jump + "}}\n" + "image: nginx\nroutes: [{hostname: ledger.example.com, port: 8080}]\n" } diff --git a/internal/app/load.go b/internal/app/load.go index e3cae4aa..cfa47951 100644 --- a/internal/app/load.go +++ b/internal/app/load.go @@ -16,7 +16,7 @@ import ( ) // APIVersion is the only authoring contract this package accepts. -const APIVersion = "onebox.run/v2" +const APIVersion = "onebox.run/v1" // maxDerivedName is an Onebox limit chosen for headroom, not a container-runtime // maximum. An over-long name is refused rather than truncated: truncation with a @@ -67,18 +67,6 @@ func Load(path string) (*Spec, error) { // LoadBytes runs the fixed pipeline: parse, expand, validate, then apply the // cross-field rules the schema cannot express. func LoadBytes(b []byte, filename string) (*Spec, error) { - return loadBytes(b, filename, false) -} - -// LoadReleaseSnapshotBytes loads an immutable project snapshot written by an -// earlier Onebox release. It accepts v1 only on this internal replay boundary, -// migrating its route representation in memory; normal project loading remains -// strictly v2 so an authored file can never opt into retired semantics. -func LoadReleaseSnapshotBytes(b []byte, filename string) (*Spec, error) { - return loadBytes(b, filename, true) -} - -func loadBytes(b []byte, filename string, allowV1Snapshot bool) (*Spec, error) { var raw map[string]any if err := yaml.Unmarshal(b, &raw); err != nil { return nil, errf("project_unparsable", filename, "", "invalid YAML: %v", firstLine(err.Error())) @@ -95,13 +83,6 @@ func loadBytes(b []byte, filename string, allowV1Snapshot bool) (*Spec, error) { lines = lineIndex(&doc) } - legacyV1Snapshot := false - if allowV1Snapshot && raw["api_version"] == "onebox.run/v1" { - if err := migrateV1ReleaseSnapshot(raw); err != nil { - return nil, err - } - legacyV1Snapshot = true - } if err := checkAPIVersion(raw); err != nil { return nil, err } @@ -141,7 +122,6 @@ func loadBytes(b []byte, filename string, allowV1Snapshot bool) (*Spec, error) { // because the enumeration then promises a failure that never fires. p.Dir = filepath.Dir(filename) p.file = filename - p.legacyV1Snapshot = legacyV1Snapshot if err := validateSpec(p); err != nil { return nil, err } @@ -169,100 +149,6 @@ func checkAPIVersion(raw map[string]any) error { // shorthandKeys are the top-level fields that describe a single workload. var shorthandKeys = []string{"build", "image", "compose", "port", "health", "routes"} -func migrateV1ReleaseSnapshot(raw map[string]any) error { - raw["api_version"] = APIVersion - if err := migrateV1Workload(raw, "workload shorthand"); err != nil { - return err - } - workloads, ok := raw["workloads"].(map[string]any) - if ok { - for name, value := range workloads { - workload, ok := value.(map[string]any) - if !ok { - continue - } - if err := migrateV1Workload(workload, "workloads."+name); err != nil { - return err - } - } - } - environments, ok := raw["environments"].(map[string]any) - if !ok { - return nil - } - for environmentName, value := range environments { - environment, ok := value.(map[string]any) - if !ok { - continue - } - overrides, ok := environment["overrides"].(map[string]any) - if !ok { - continue - } - workloadOverrides, ok := overrides["workloads"].(map[string]any) - if !ok { - continue - } - for workloadName, patch := range workloadOverrides { - workload, ok := patch.(map[string]any) - if !ok { - continue - } - path := "environments." + environmentName + ".overrides.workloads." + workloadName - if err := migrateV1Workload(workload, path); err != nil { - return err - } - } - } - return nil -} - -func migrateV1Workload(workload map[string]any, path string) error { - domain, hasDomain := workload["domain"] - port, hasPort := workload["port"] - _, hasRoutes := workload["routes"] - if hasRoutes && (hasDomain || hasPort) { - return errf("project_invalid", path, "", "v1 snapshot declares both domain/port and routes") - } - if hasDomain != hasPort { - return errf("project_invalid", path, "", "v1 snapshot must declare domain and port together") - } - if hasDomain { - workload["routes"] = []any{map[string]any{"hostname": domain, "port": port}} - delete(workload, "domain") - return nil - } - routes, ok := workload["routes"].([]any) - if !ok { - return nil - } - for i, value := range routes { - route, ok := value.(map[string]any) - if !ok { - continue - } - domain, hasDomain := route["domain"] - suffix, hasSuffix := route["wildcard_suffix"] - _, hasHostname := route["hostname"] - if hasHostname || hasDomain == hasSuffix { - return errf("project_invalid", indexed(path+".routes", i), "", - "v1 snapshot route must declare exactly one of domain or wildcard_suffix") - } - if hasDomain { - route["hostname"] = domain - delete(route, "domain") - } else { - text, ok := suffix.(string) - if !ok { - return errf("project_invalid", indexed(path+".routes", i)+".wildcard_suffix", "", "v1 wildcard suffix must be a string") - } - route["hostname"] = "*." + text - delete(route, "wildcard_suffix") - } - } - return nil -} - // expand rewrites shorthand into the normalised form the schema validates. It // runs before validation because the schema requires discriminators — a role // left absent would keep every branch of the workload disjunction alive. diff --git a/internal/app/load_test.go b/internal/app/load_test.go index 8ac008b3..0b549c6d 100644 --- a/internal/app/load_test.go +++ b/internal/app/load_test.go @@ -8,86 +8,15 @@ import ( "testing" ) -const base = "api_version: onebox.run/v2\napp: ledger\nenvironments: {production: {server: root@1.2.3.4}}\n" +const base = "api_version: onebox.run/v1\napp: ledger\nenvironments: {production: {server: root@1.2.3.4}}\n" const min = base + "build: .\nroutes: [{hostname: ledger.example.com, port: 8080}]\n" func wl(body string) string { return base + "workloads: {" + body + "}\n" } -func TestAPIVersionV2IsRequired(t *testing.T) { - _, err := LoadBytes([]byte(strings.Replace(min, APIVersion, "onebox.run/v1", 1)), "ob.yml") +func TestAPIVersionV1IsRequired(t *testing.T) { + _, err := LoadBytes([]byte(strings.Replace(min, APIVersion, "onebox.run/v2", 1)), "ob.yml") if err == nil || !strings.Contains(err.Error(), "schema_identity_unsupported") { - t.Fatalf("v1 project must be rejected with a version error: %v", err) - } -} - -func TestReleaseSnapshotLoaderMigratesV1RoutesOnlyAtReplayBoundary(t *testing.T) { - legacy := `api_version: onebox.run/v1 -app: ledger -environments: {production: {server: root@1.2.3.4}} -image: nginx -domain: Example.COM. -port: 8080 -` - p, err := LoadReleaseSnapshotBytes([]byte(legacy), "/var/lib/ob/ledger/releases/old/ob.snapshot.yml") - if err != nil { - t.Fatalf("load v1 release snapshot: %v", err) - } - routes := p.Workloads["ledger"].NormalisedRoutes() - if p.APIVersion != APIVersion || len(routes) != 1 || routes[0].Hostname != "Example.COM." || routes[0].Port != 8080 { - t.Fatalf("migrated snapshot = version %q, routes %+v", p.APIVersion, routes) - } - resolved, err := p.Resolve("production") - if err != nil { - t.Fatalf("resolve migrated v1 snapshot: %v", err) - } - if got := resolved.Workloads["ledger"].Routes[0].Hostname; got != "Example.COM." { - t.Fatalf("resolved legacy hostname = %q", got) - } - - wildcard := `api_version: onebox.run/v1 -app: ledger -environments: - production: - server: root@1.2.3.4 - overrides: - workloads: - web: - routes: [{wildcard_suffix: branch.example.com, path: /, port: 8081, entrypoint: websecure, protocol: http, scheme: http, tls: none}] -workloads: - web: - image: nginx - routes: [{wildcard_suffix: preview.example.com, port: 8080, tls: none}] -` - p, err = LoadReleaseSnapshotBytes([]byte(wildcard), "ob.snapshot.yml") - if err != nil { - t.Fatalf("load v1 wildcard snapshot: %v", err) - } - if got := p.Workloads["web"].Routes[0].Hostname; got != "*.preview.example.com" { - t.Fatalf("migrated wildcard hostname = %q", got) - } - resolved, err = p.Resolve("production") - if err != nil { - t.Fatalf("resolve v1 wildcard override: %v", err) - } - if got := resolved.Workloads["web"].Routes[0].Hostname; got != "*.branch.example.com" { - t.Fatalf("migrated wildcard override hostname = %q", got) - } - maxV1Suffix := strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + - strings.Repeat("c", 63) + "." + strings.Repeat("d", 61) - maxWildcard := strings.Replace(wildcard, "preview.example.com", maxV1Suffix, 1) - if _, err := LoadReleaseSnapshotBytes([]byte(maxWildcard), "ob.snapshot.yml"); err != nil { - t.Fatalf("v1 snapshot wildcard at the v1 suffix limit: %v", err) - } - - for name, body := range map[string]string{ - "unknown version": strings.Replace(legacy, "onebox.run/v1", "onebox.run/v0", 1), - "incomplete route": strings.Replace(legacy, "port: 8080\n", "", 1), - } { - t.Run(name, func(t *testing.T) { - if _, err := LoadReleaseSnapshotBytes([]byte(body), "ob.snapshot.yml"); err == nil { - t.Fatal("invalid legacy snapshot was accepted") - } - }) + t.Fatalf("v2 project must be rejected with a version error: %v", err) } } @@ -245,10 +174,10 @@ func conformanceCases() []conformanceCase { {"explicit workloads block", wl("web: {image: nginx}"), true}, {"image reference with registry port", wl("web: {image: \"registry.example.com:5000/acme/app:1.2\"}"), true}, {"image reference with uppercase repository", wl("web: {image: \"ghcr.io/Acme/app:1.2\"}"), false}, - {"one-char identifier", "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: h}}\nimage: nginx\n", true}, - {"app starting ob-", "api_version: onebox.run/v2\napp: ob-app\nenvironments: {p: {server: h}}\nimage: nginx\n", false}, - {"host proxy name", "api_version: onebox.run/v2\napp: onebox-proxy\nenvironments: {p: {server: h}}\nimage: nginx\n", false}, - {"underscore identifier", "api_version: onebox.run/v2\napp: my_app\nenvironments: {p: {server: h}}\nimage: nginx\n", false}, + {"one-char identifier", "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: h}}\nimage: nginx\n", true}, + {"app starting ob-", "api_version: onebox.run/v1\napp: ob-app\nenvironments: {p: {server: h}}\nimage: nginx\n", false}, + {"host proxy name", "api_version: onebox.run/v1\napp: onebox-proxy\nenvironments: {p: {server: h}}\nimage: nginx\n", false}, + {"underscore identifier", "api_version: onebox.run/v1\napp: my_app\nenvironments: {p: {server: h}}\nimage: nginx\n", false}, {"unknown top-level field", min + "bogus: 1\n", false}, {"x- extension accepted", min + "x-note: anything\n", true}, {"port out of range", base + "image: nginx\nhostname: d\nport: 70000\n", false}, @@ -276,15 +205,15 @@ func conformanceCases() []conformanceCase { {"absolute env_file", min + "runtime: {env_files: [/etc/x.env]}\n", false}, {"relative env_file", min + "runtime: {env_files: [.env.production]}\n", true}, {"base_path absolute", min + "base_path: /mnt/data/ob\n", true}, - {"duration in days", "api_version: onebox.run/v2\napp: a\nimage: nginx\nenvironments: {p: {server: h, policy: {migrations: {backup_max_age: 14d}}}}\n", true}, - {"non-calver minimum version", "api_version: onebox.run/v2\napp: a\nimage: nginx\nenvironments: {p: {server: h, policy: {min_onebox_version: 0.0.1-m0}}}\n", false}, - {"incomplete plan schema", "api_version: onebox.run/v2\napp: a\nimage: nginx\nenvironments: {p: {server: h, policy: {min_plan_schema: \"onebox.run/executable-deploy-plan/v1alpha\"}}}\n", false}, + {"duration in days", "api_version: onebox.run/v1\napp: a\nimage: nginx\nenvironments: {p: {server: h, policy: {migrations: {backup_max_age: 14d}}}}\n", true}, + {"non-calver minimum version", "api_version: onebox.run/v1\napp: a\nimage: nginx\nenvironments: {p: {server: h, policy: {min_onebox_version: 0.0.1-m0}}}\n", false}, + {"incomplete plan schema", "api_version: onebox.run/v1\napp: a\nimage: nginx\nenvironments: {p: {server: h, policy: {min_plan_schema: \"onebox.run/executable-deploy-plan/v1alpha\"}}}\n", false}, {"hook with local", min + "hooks: {pre_release: {run: scripts/build.sh, local: true}}\n", true}, // A hook key is a lifecycle seam OR a declared job name. Both halves need // a case: an unlisted seam loads and never fires, and refusing a job name // would break the per-job command override the engine reads. {"hook naming an unlisted seam", min + "hooks: {pre_deploy: {run: scripts/backup.sh}}\n", false}, - {"hook naming a declared job", "api_version: onebox.run/v2\napp: a\nenvironments: {p: {server: h}}\nhooks: {migrate: {run: ./bin/migrate}}\nworkloads:\n w: {role: application, image: nginx}\n migrate: {role: job, image: nginx, data_effect: migration}\n", true}, + {"hook naming a declared job", "api_version: onebox.run/v1\napp: a\nenvironments: {p: {server: h}}\nhooks: {migrate: {run: ./bin/migrate}}\nworkloads:\n w: {role: application, image: nginx}\n migrate: {role: job, image: nginx, data_effect: migration}\n", true}, {"hook naming neither", min + "hooks: {typo_hook: {run: scripts/x.sh}}\n", false}, // A settings key is interpolated into a generated shell command without // quoting, so the grammar is the only thing between a project file and @@ -328,7 +257,7 @@ func conformanceCases() []conformanceCase { {"encrypted env file entry", min + "runtime: {env_files: [{file: secrets.env, provider: sops}]}\n", true}, {"unknown env file provider", min + "runtime: {env_files: [{file: s.env, provider: vault}]}\n", false}, {"env file entry without a file", min + "runtime: {env_files: [{provider: sops}]}\n", false}, - {"environment-scoped env files", "api_version: onebox.run/v2\napp: a\nimage: nginx\nenvironments: {p: {server: h, env_files: [.env.p]}}\n", true}, + {"environment-scoped env files", "api_version: onebox.run/v1\napp: a\nimage: nginx\nenvironments: {p: {server: h, env_files: [.env.p]}}\n", true}, {"http check without a path", min + "checks: {http: [{workload: ledger}]}\n", false}, {"url check carrying an exec field", min + "checks: {url: [{url: \"https://x/\", run: \"echo\"}]}\n", false}, {"url check with contains and advisory", min + "checks: {url: [{url: \"https://x/\", contains: \" 253 { - return errf("project_invalid", path, "", "%q is not a DNS hostname: its suffix exceeds 253 characters", value) - } - return gRouteHostname.check(path, value) - } +func validateRouteHostname(path, value string) error { if len(value) > 253 { return errf("project_invalid", path, "", "%q is not a DNS hostname: it exceeds 253 characters", value) } diff --git a/internal/app/workload_contract_test.go b/internal/app/workload_contract_test.go index 0442f3ae..f880b664 100644 --- a/internal/app/workload_contract_test.go +++ b/internal/app/workload_contract_test.go @@ -54,7 +54,7 @@ func TestSecretInputRevisionsAreScopedByWorkload(t *testing.T) { t.Fatal(err) } } - spec, err := LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -94,7 +94,7 @@ func TestSecretInputRevisionsUseTheProvidedSnapshot(t *testing.T) { if err := os.WriteFile(path, []byte("cipher-before"), 0o600); err != nil { t.Fatal(err) } - spec, err := LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: diff --git a/internal/engine/backup_identity_test.go b/internal/engine/backup_identity_test.go index 58a333d3..3ade3012 100644 --- a/internal/engine/backup_identity_test.go +++ b/internal/engine/backup_identity_test.go @@ -10,7 +10,7 @@ import ( "github.com/labstack/onebox/internal/transport" ) -const protectedPostgresProject = `api_version: onebox.run/v2 +const protectedPostgresProject = `api_version: onebox.run/v1 app: shop environments: production: {server: deploy@example.net} diff --git a/internal/engine/deploy.go b/internal/engine/deploy.go index 479fdc27..8636a148 100644 --- a/internal/engine/deploy.go +++ b/internal/engine/deploy.go @@ -117,6 +117,14 @@ func (e *Engine) deployCore(ctx context.Context, releaseID, localStagingDir stri if err := e.requireServingApplicationManifest(ctx, prev); err != nil { return err } + // A predecessor is replayed after activation to retire workloads removed by + // this release. Validate it before any proxy or workload mutation so a strict + // schema refusal cannot leave the new release serving with cleanup impossible. + if prev != "" { + if _, err := e.engineFromReleaseSnapshotFor(ctx, prev, "deploy"); err != nil { + return err + } + } // After preflight, so an unreachable daemon is reported by the check that // exists for it rather than by a raw `docker ps` failure — and still before // any workload is rolled or any gate job runs. diff --git a/internal/engine/deploy_test.go b/internal/engine/deploy_test.go index 1573f887..7b2533e3 100644 --- a/internal/engine/deploy_test.go +++ b/internal/engine/deploy_test.go @@ -23,7 +23,7 @@ import ( const guardedHealthcheck = `["CMD-SHELL","[ -f /tmp/ob-drain ] \u0026\u0026 exit 1; curl -fsS 'http://127.0.0.1:80/'"]` const enginePreviousFrontendProject = ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample environments: production: @@ -225,6 +225,34 @@ func happyFake() *transport.Fake { return f } +func TestDeployRefusesUnsupportedPredecessorSnapshotBeforeRuntimeMutation(t *testing.T) { + f := happyFake() + base := f.Dynamic + f.Dynamic = func(command string) (transport.Result, bool) { + switch { + case strings.Contains(command, "readlink"): + return transport.Result{Stdout: "releases/" + engineTestPreviousReleaseID + "\n"}, true + case strings.Contains(command, "/"+engineTestPreviousReleaseID+"/ob.snapshot.yml"): + return transport.Result{Stdout: strings.Replace(enginePreviousFrontendProject, app.APIVersion, "onebox.run/v2", 1)}, true + } + return base(command) + } + + e := New(testConfig(), testProject(t), f, Options{Out: &bytes.Buffer{}, Sleep: noSleep}) + err := e.Deploy(context.Background(), engineTestDeployReleaseID, t.TempDir()) + if err == nil || !strings.Contains(err.Error(), "snapshot unusable") || !strings.Contains(err.Error(), "schema_identity_unsupported") { + t.Fatalf("unsupported predecessor error = %v", err) + } + if len(f.Uploads) != 0 { + t.Fatalf("unsupported predecessor uploaded release files: %v", f.Uploads) + } + for _, command := range f.Commands { + if strings.Contains(command, "docker restart onebox-proxy") || strings.Contains(command, "ln -sfn") { + t.Fatalf("unsupported predecessor reached runtime mutation: %s", command) + } + } +} + func TestDeployRetainsPlannedWorkloadWithoutRuntimeMutation(t *testing.T) { f := happyFake() revision := "sha256:" + strings.Repeat("a", 64) diff --git a/internal/engine/fixtures_test.go b/internal/engine/fixtures_test.go index 4575610e..667d90d6 100644 --- a/internal/engine/fixtures_test.go +++ b/internal/engine/fixtures_test.go @@ -21,7 +21,7 @@ const ( // that assembles the struct directly can assert on a shape the loader would // never produce. const engineProject = ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample environments: production: diff --git a/internal/engine/host_environment_test.go b/internal/engine/host_environment_test.go index 79d1c1e0..1a86cbe7 100644 --- a/internal/engine/host_environment_test.go +++ b/internal/engine/host_environment_test.go @@ -114,7 +114,7 @@ func TestHostOwnerRecordRoundTrips(t *testing.T) { // host owner record. func TestEnvironmentSelectsTheBasePath(t *testing.T) { spec, err := app.LoadBytes([]byte(` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample base_path: /var/lib/ob environments: diff --git a/internal/engine/recovery.go b/internal/engine/recovery.go index 2d5a8aa2..534eb944 100644 --- a/internal/engine/recovery.go +++ b/internal/engine/recovery.go @@ -44,7 +44,7 @@ func (e *Engine) engineFromReleaseSnapshotFor(ctx context.Context, releaseID, op return nil, fmt.Errorf("%s refused: release %s snapshot is empty", operation, releaseID) } - snapshot, err := app.LoadReleaseSnapshotBytes([]byte(res.Stdout), path) + snapshot, err := app.LoadBytes([]byte(res.Stdout), path) if err != nil { return nil, fmt.Errorf("%s refused: release %s snapshot unusable: %w", operation, releaseID, err) } diff --git a/internal/engine/recovery_test.go b/internal/engine/recovery_test.go index f89fb944..d5c5a84f 100644 --- a/internal/engine/recovery_test.go +++ b/internal/engine/recovery_test.go @@ -49,37 +49,6 @@ func recoveryWriter(engine *Engine) *journal.Writer { return &journal.Writer{T: engine.T, Names: engine.Names(), DeployID: engineTestDeployReleaseID, Epoch: 2} } -func TestLifecycleReplayLoadsV1ReleaseSnapshot(t *testing.T) { - target := happyFake() - base := target.Dynamic - target.Dynamic = func(command string) (transport.Result, bool) { - if strings.Contains(command, "/releases/legacy/ob.snapshot.yml") { - return transport.Result{Stdout: `api_version: onebox.run/v1 -app: sample -environments: - production: - server: deploy@h - overrides: - workloads: - sample: - routes: [{domain: override.example.com, path: /, port: 8081, entrypoint: websecure, protocol: http, scheme: http, tls: none}] -image: nginx -routes: [{domain: Example.COM., port: 8080}] -`}, true - } - return base(command) - } - engine := New(testConfig(), testProject(t), target, Options{Environment: "production"}) - replay, err := engine.engineFromReleaseSnapshotFor(context.Background(), "legacy", "retired-workload cleanup") - if err != nil { - t.Fatalf("load v1 lifecycle snapshot: %v", err) - } - routes := replay.Spec.Workloads["sample"].Routes - if len(routes) != 1 || routes[0].Hostname != "override.example.com" || routes[0].Port != 8081 { - t.Fatalf("replayed routes = %+v", routes) - } -} - func TestRecoveryRetryKeepsCheckpointUntilHealthyAndSweepsStaleRoles(t *testing.T) { target := happyFake() verifyCalls := 0 diff --git a/internal/engine/resume_test.go b/internal/engine/resume_test.go index 3167c1ca..065e52f1 100644 --- a/internal/engine/resume_test.go +++ b/internal/engine/resume_test.go @@ -390,7 +390,7 @@ func TestAbortUsesInterruptedExpandOnlyPolicyAfterConfigEdit(t *testing.T) { } const interruptedWebSnapshot = ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample environments: { production: { server: deploy@h } } workloads: diff --git a/internal/engine/secret_generation_rolling_test.go b/internal/engine/secret_generation_rolling_test.go index d8c4cd37..1557efb6 100644 --- a/internal/engine/secret_generation_rolling_test.go +++ b/internal/engine/secret_generation_rolling_test.go @@ -14,7 +14,7 @@ import ( // web declares a health check, so it defaults to rolling; worker stays a // recreate workload, which is what keeps the two paths visible in one push. -const rollingGenerationProject = `api_version: onebox.run/v2 +const rollingGenerationProject = `api_version: onebox.run/v1 app: shop base_path: /srv/onebox environments: diff --git a/internal/engine/secret_generation_test.go b/internal/engine/secret_generation_test.go index 239546a0..2a864a13 100644 --- a/internal/engine/secret_generation_test.go +++ b/internal/engine/secret_generation_test.go @@ -13,7 +13,7 @@ import ( "github.com/labstack/onebox/internal/transport" ) -const generationProject = `api_version: onebox.run/v2 +const generationProject = `api_version: onebox.run/v1 app: shop base_path: /srv/onebox environments: diff --git a/internal/engine/secretspush_test.go b/internal/engine/secretspush_test.go index 6be31561..d35c0851 100644 --- a/internal/engine/secretspush_test.go +++ b/internal/engine/secretspush_test.go @@ -11,7 +11,7 @@ import ( "github.com/labstack/onebox/internal/transport" ) -const secretGraphProject = `api_version: onebox.run/v2 +const secretGraphProject = `api_version: onebox.run/v1 app: shop environments: production: {server: deploy@example.invalid} diff --git a/internal/engine/verify_injection_test.go b/internal/engine/verify_injection_test.go index 4709e6b6..20f22515 100644 --- a/internal/engine/verify_injection_test.go +++ b/internal/engine/verify_injection_test.go @@ -64,7 +64,7 @@ func TestVerifyHTTPProbeStillCarriesThePortAndPath(t *testing.T) { func verificationProject(t *testing.T, path string) *app.Resolved { t.Helper() spec, err := app.LoadBytes([]byte(` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample environments: production: diff --git a/internal/onebox/bootstrap_test.go b/internal/onebox/bootstrap_test.go index 7b7a2368..41a8e788 100644 --- a/internal/onebox/bootstrap_test.go +++ b/internal/onebox/bootstrap_test.go @@ -10,7 +10,7 @@ import ( "github.com/labstack/onebox/internal/transport" ) -const bootstrapBuildProject = `api_version: onebox.run/v2 +const bootstrapBuildProject = `api_version: onebox.run/v1 app: demo environments: {production: {server: deploy@example.invalid}} runtime: diff --git a/internal/onebox/exec_test.go b/internal/onebox/exec_test.go index 870b69a8..e30ac920 100644 --- a/internal/onebox/exec_test.go +++ b/internal/onebox/exec_test.go @@ -14,7 +14,7 @@ import ( "github.com/labstack/onebox/internal/transport" ) -const execProjectYAML = `api_version: onebox.run/v2 +const execProjectYAML = `api_version: onebox.run/v1 app: shop environments: production: diff --git a/internal/onebox/jump_route_test.go b/internal/onebox/jump_route_test.go index 6d77317e..630b3107 100644 --- a/internal/onebox/jump_route_test.go +++ b/internal/onebox/jump_route_test.go @@ -16,7 +16,7 @@ func writeJumpProject(t *testing.T) string { dir := t.TempDir() path := filepath.Join(dir, "ob.yml") body := ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: demo environments: production: @@ -83,7 +83,7 @@ func TestChangingOnlyTheJumpChangesTheBinding(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "ob.yml") body := ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: demo environments: production: diff --git a/internal/onebox/load_service_runtime_test.go b/internal/onebox/load_service_runtime_test.go index 1f485c75..765ba60a 100644 --- a/internal/onebox/load_service_runtime_test.go +++ b/internal/onebox/load_service_runtime_test.go @@ -16,7 +16,7 @@ func protectedRuntimeProject(t *testing.T) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "ob.yml") - body := `api_version: onebox.run/v2 + body := `api_version: onebox.run/v1 app: example environments: {production: {server: root@host}} workloads: {web: {image: nginx:1}} diff --git a/internal/onebox/operation_graph_test.go b/internal/onebox/operation_graph_test.go index 9d832edd..443f0b2f 100644 --- a/internal/onebox/operation_graph_test.go +++ b/internal/onebox/operation_graph_test.go @@ -82,7 +82,7 @@ func TestDeploymentGraphNeverContainsHookBodies(t *testing.T) { func TestDeploymentGraphOmitsAbsentHooksAndJobs(t *testing.T) { t.Parallel() spec, err := app.LoadBytes([]byte(` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample environments: {production: {server: root@h}} workloads: @@ -118,7 +118,7 @@ func TestDeploymentClassificationDoesNotOverstateFirstDeployRollback(t *testing. func operationGraphConfig() *app.Resolved { spec, err := app.LoadBytes([]byte(` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: sample environments: {production: {server: root@h}} workloads: diff --git a/internal/onebox/secrets_push_test.go b/internal/onebox/secrets_push_test.go index ec34ca13..512444cf 100644 --- a/internal/onebox/secrets_push_test.go +++ b/internal/onebox/secrets_push_test.go @@ -12,7 +12,7 @@ import ( "github.com/labstack/onebox/internal/transport" ) -const pushProjectYAML = `api_version: onebox.run/v2 +const pushProjectYAML = `api_version: onebox.run/v1 app: shop environments: production: @@ -172,7 +172,7 @@ func TestSecretsPushRotatesEveryEntry(t *testing.T) { // A project with nothing encrypted is told so, rather than reporting a push. func TestSecretsPushWithNothingEncryptedIsRefused(t *testing.T) { dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "ob.yml"), []byte(`api_version: onebox.run/v2 + if err := os.WriteFile(filepath.Join(dir, "ob.yml"), []byte(`api_version: onebox.run/v1 app: shop environments: {production: {server: deploy@example.invalid}} workloads: diff --git a/internal/onebox/service_test.go b/internal/onebox/service_test.go index bc2eae19..827873e7 100644 --- a/internal/onebox/service_test.go +++ b/internal/onebox/service_test.go @@ -27,7 +27,7 @@ services: image: ghcr.io/example/postgres:` + testSecret + ` `, "ob.yml": ` -api_version: onebox.run/v2 +api_version: onebox.run/v1 app: demo environments: production: @@ -164,7 +164,7 @@ func writeComposeBuildProject(t *testing.T) string { pinnedWeb := "ghcr.io/example/app@sha256:" + strings.Repeat("1", 64) files := map[string]string{ "compose.yaml": "services:\n database:\n build: .\n command: [postgres, -c, shared_buffers=256MB]\n", - "ob.yml": `api_version: onebox.run/v2 + "ob.yml": `api_version: onebox.run/v1 app: demo environments: {production: {server: deploy@example.invalid}} workloads: @@ -232,7 +232,7 @@ func TestPlanDeployUsesDeployedSecretGraphDuringTransition(t *testing.T) { if workerSecret { workerEnv = "\n env_files: [{file: worker.enc.env, provider: sops}]" } - return `api_version: onebox.run/v2 + return `api_version: onebox.run/v1 app: demo environments: production: {server: deploy@example.invalid} diff --git a/internal/onebox/staging_secrets_test.go b/internal/onebox/staging_secrets_test.go index 9370516b..334bf02c 100644 --- a/internal/onebox/staging_secrets_test.go +++ b/internal/onebox/staging_secrets_test.go @@ -38,7 +38,7 @@ func twoEncryptedEntries(t *testing.T) string { write("api.enc.env", "TOKEN=api-token\n") write("worker.enc.env", "TOKEN=worker-token\n") write("shared.env", "REGION=eu\n") - write("ob.yml", `api_version: onebox.run/v2 + write("ob.yml", `api_version: onebox.run/v1 app: shop environments: production: @@ -252,7 +252,7 @@ func TestExternalServiceConnectionIsProjectedLeastPrivilegeIntoRelease(t *testin if err := os.WriteFile(filepath.Join(dir, "secrets", "database.env"), []byte(secret), 0o600); err != nil { t.Fatal(err) } - project := `api_version: onebox.run/v2 + project := `api_version: onebox.run/v1 app: shop environments: {production: {server: root@h}} workloads: diff --git a/internal/onebox/workload_contract_test.go b/internal/onebox/workload_contract_test.go index 914121b0..f6df03d6 100644 --- a/internal/onebox/workload_contract_test.go +++ b/internal/onebox/workload_contract_test.go @@ -15,7 +15,7 @@ func TestWorkloadContractsScopePlainEnvironmentChanges(t *testing.T) { t.Fatal(err) } } - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -83,7 +83,7 @@ func TestWorkloadContractsTrackRelativeBindMountContent(t *testing.T) { t.Fatal(err) } } - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -121,7 +121,7 @@ deployment: {order: [api, worker]} } func TestBindMountContractIsIndependentOfWhereTheReleaseIsStaged(t *testing.T) { - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -157,7 +157,7 @@ deployment: {order: [api]} func TestWorkloadContractsIgnoreVolumesOnAnAdoptedComposeService(t *testing.T) { staging := t.TempDir() - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -177,7 +177,7 @@ deployment: {order: [api]} } func TestBindMountContractNoticesAnAddedEmptyDirectory(t *testing.T) { - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -216,7 +216,7 @@ deployment: {order: [api]} func bindMountRevision(t *testing.T, mode os.FileMode) string { t.Helper() - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: diff --git a/internal/onebox/workload_plan_test.go b/internal/onebox/workload_plan_test.go index ea305623..89fa5df3 100644 --- a/internal/onebox/workload_plan_test.go +++ b/internal/onebox/workload_plan_test.go @@ -15,7 +15,7 @@ import ( func workloadPlanFixture(t *testing.T) (*app.Resolved, string, []OperationStep, engine.HostState) { t.Helper() - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: @@ -163,7 +163,7 @@ func TestPlanDeployRetainsUnchangedWorkerWhenAnotherWorkloadChanges(t *testing.T digestB := strings.Repeat("2", 64) workerDigest := strings.Repeat("3", 64) project := func(apiDigest string) string { - return `api_version: onebox.run/v2 + return `api_version: onebox.run/v1 app: sample environments: {production: {server: deploy@example.test}} workloads: diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 877e9b4c..b728df02 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -160,7 +160,7 @@ func TestManagedWildcardTLSRejectsIncompleteDNSConfiguration(t *testing.T) { } func TestManagedTLSRouterReferencesDefaultStaticResolver(t *testing.T) { - spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v2 + spec, err := app.LoadBytes([]byte(`api_version: onebox.run/v1 app: sample environments: production: {server: root@example.com} diff --git a/site/public/onebox.run-v1.schema.json b/site/public/onebox.run-v1.schema.json index df4232c1..9a1182b1 100644 --- a/site/public/onebox.run-v1.schema.json +++ b/site/public/onebox.run-v1.schema.json @@ -58,11 +58,6 @@ "compose" ] }, - { - "required": [ - "domain" - ] - }, { "required": [ "port" @@ -577,14 +572,6 @@ }, "type": "object" }, - "domain": { - "description": "Domain shorthand for one HTTPS route; requires port and cannot be combined with routes. Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "examples": [ - "shop.example.com" - ], - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$", - "type": "string" - }, "environments": { "additionalProperties": { "additionalProperties": false, @@ -1154,7 +1141,7 @@ "type": "object" }, "port": { - "description": "Container port used with domain shorthand and as the default HTTP health port.", + "description": "Default container port used by HTTP health checks.", "examples": [ 3000 ], @@ -1302,12 +1289,12 @@ { "if": { "properties": { - "domain": { + "hostname": { "const": "*" } }, "required": [ - "domain" + "hostname" ] }, "then": { @@ -1327,55 +1314,74 @@ "tls" ] } - } - ], - "oneOf": [ + }, { - "not": { + "if": { + "properties": { + "hostname": { + "pattern": "^\\*\\." + } + }, "required": [ - "wildcard_suffix" + "hostname" ] }, - "required": [ - "domain" - ] + "then": { + "properties": { + "protocol": { + "const": "http" + } + } + } }, { - "not": { + "if": { + "properties": { + "tls": { + "const": "passthrough" + } + }, "required": [ - "domain" + "tls" ] }, - "required": [ - "wildcard_suffix" - ] + "then": { + "properties": { + "protocol": { + "const": "tcp" + } + }, + "required": [ + "protocol" + ] + } } ], "patternProperties": { "^x-": {} }, "properties": { - "domain": { + "entrypoint": { + "default": "websecure", + "description": "Named proxy listener used for the route.", + "type": "string" + }, + "hostname": { "anyOf": [ { - "description": "Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$" + "maxLength": 253, + "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" }, { "const": "*" } ], - "description": "Exact DNS name matched by the proxy. Mutually exclusive with wildcard_suffix.", + "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", "examples": [ "shop.example.com" ], "type": "string" }, - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, "middlewares": { "description": "Ordered provider-qualified middleware references applied to this route.", "items": { @@ -1402,7 +1408,7 @@ }, "protocol": { "default": "http", - "description": "Routing protocol: http, tcp, or udp.", + "description": "Routing protocol: http or tcp.", "enum": [ "http", "tcp" @@ -1411,7 +1417,7 @@ }, "scheme": { "default": "http", - "description": "Backend connection scheme: http, https, h2c, tcp, or udp.", + "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", "enum": [ "http", "https", @@ -1428,17 +1434,11 @@ "none" ], "type": "string" - }, - "wildcard_suffix": { - "description": "DNS suffix whose immediate subdomains are matched. For example, example.com matches shop.example.com but not example.com or a.b.example.com. Mutually exclusive with domain.", - "examples": [ - "preview.example.com" - ], - "maxLength": 253, - "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$", - "type": "string" } }, + "required": [ + "hostname" + ], "type": "object" }, "type": "array" @@ -1853,55 +1853,6 @@ } ] }, - { - "not": { - "allOf": [ - { - "anyOf": [ - { - "required": [ - "domain" - ] - }, - { - "required": [ - "port" - ] - } - ] - }, - { - "required": [ - "routes" - ] - } - ] - } - }, - { - "if": { - "required": [ - "domain" - ] - }, - "then": { - "required": [ - "port" - ] - } - }, - { - "if": { - "required": [ - "port" - ] - }, - "then": { - "required": [ - "domain" - ] - } - }, { "not": { "allOf": [ @@ -2157,14 +2108,6 @@ ], "type": "string" }, - "domain": { - "description": "Domain shorthand for one HTTPS route; requires port and cannot be combined with routes. Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "examples": [ - "shop.example.com" - ], - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$", - "type": "string" - }, "drain": { "additionalProperties": false, "description": "Signal and timing used to remove a container from traffic before stopping it.", @@ -2665,7 +2608,7 @@ "type": "object" }, "port": { - "description": "Container port used with domain shorthand and as the default HTTP health port.", + "description": "Default container port used by HTTP health checks.", "examples": [ 3000 ], @@ -2774,12 +2717,12 @@ { "if": { "properties": { - "domain": { + "hostname": { "const": "*" } }, "required": [ - "domain" + "hostname" ] }, "then": { @@ -2799,55 +2742,74 @@ "tls" ] } - } - ], - "oneOf": [ + }, { - "not": { + "if": { + "properties": { + "hostname": { + "pattern": "^\\*\\." + } + }, "required": [ - "wildcard_suffix" + "hostname" ] }, - "required": [ - "domain" - ] + "then": { + "properties": { + "protocol": { + "const": "http" + } + } + } }, { - "not": { + "if": { + "properties": { + "tls": { + "const": "passthrough" + } + }, "required": [ - "domain" + "tls" ] }, - "required": [ - "wildcard_suffix" - ] + "then": { + "properties": { + "protocol": { + "const": "tcp" + } + }, + "required": [ + "protocol" + ] + } } ], "patternProperties": { "^x-": {} }, "properties": { - "domain": { + "entrypoint": { + "default": "websecure", + "description": "Named proxy listener used for the route.", + "type": "string" + }, + "hostname": { "anyOf": [ { - "description": "Expects an exact host with no wildcard, control character or backtick; use wildcard_suffix for wildcard routing.", - "pattern": "^[^\\x00-\\x1f\\x7f`*]+$" + "maxLength": 253, + "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" }, { "const": "*" } ], - "description": "Exact DNS name matched by the proxy. Mutually exclusive with wildcard_suffix.", + "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", "examples": [ "shop.example.com" ], "type": "string" }, - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, "middlewares": { "description": "Ordered provider-qualified middleware references applied to this route.", "items": { @@ -2874,7 +2836,7 @@ }, "protocol": { "default": "http", - "description": "Routing protocol: http, tcp, or udp.", + "description": "Routing protocol: http or tcp.", "enum": [ "http", "tcp" @@ -2883,7 +2845,7 @@ }, "scheme": { "default": "http", - "description": "Backend connection scheme: http, https, h2c, tcp, or udp.", + "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", "enum": [ "http", "https", @@ -2900,17 +2862,11 @@ "none" ], "type": "string" - }, - "wildcard_suffix": { - "description": "DNS suffix whose immediate subdomains are matched. For example, example.com matches shop.example.com but not example.com or a.b.example.com. Mutually exclusive with domain.", - "examples": [ - "preview.example.com" - ], - "maxLength": 253, - "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$", - "type": "string" } }, + "required": [ + "hostname" + ], "type": "object" }, "type": "array" diff --git a/site/public/onebox.run-v2.schema.json b/site/public/onebox.run-v2.schema.json deleted file mode 100644 index 5786c127..00000000 --- a/site/public/onebox.run-v2.schema.json +++ /dev/null @@ -1,3119 +0,0 @@ -{ - "$id": "https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "additionalProperties": false, - "anyOf": [ - { - "properties": { - "workloads": { - "minProperties": 1 - } - }, - "required": [ - "workloads" - ] - }, - { - "anyOf": [ - { - "required": [ - "build" - ] - }, - { - "required": [ - "image" - ] - }, - { - "required": [ - "compose" - ] - } - ] - } - ], - "description": "One application, its workloads, the services it needs, and how a release rolls out.", - "not": { - "allOf": [ - { - "required": [ - "workloads" - ] - }, - { - "anyOf": [ - { - "required": [ - "build" - ] - }, - { - "required": [ - "image" - ] - }, - { - "required": [ - "compose" - ] - }, - { - "required": [ - "port" - ] - }, - { - "required": [ - "health" - ] - }, - { - "required": [ - "routes" - ] - } - ] - } - ] - }, - "patternProperties": { - "^x-": {} - }, - "properties": { - "api_version": { - "const": "onebox.run/v2", - "description": "Project contract version. Must be onebox.run/v2.", - "examples": [ - "onebox.run/v2" - ], - "type": "string" - }, - "app": { - "description": "Stable application name used in generated container, volume, network, and host paths. The application's name. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters, and may not begin \"ob-\" or be a name the host layout reserves.", - "examples": [ - "shop" - ], - "not": { - "anyOf": [ - { - "pattern": "^ob-" - }, - { - "const": "ob" - }, - { - "const": "onebox-proxy" - }, - { - "const": "_host" - } - ] - }, - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "backup_targets": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "bucket": { - "description": "Existing destination bucket used by this target. Expects a lower-case S3-compatible bucket name between 3 and 63 characters.", - "examples": [ - "onebox-backups" - ], - "pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", - "type": "string" - }, - "credentials": { - "additionalProperties": false, - "description": "Trusted encrypted-file entries containing destination credentials; values never appear in the project.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "access_key_entry": { - "description": "Variable name containing the destination access key. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "BACKUP_ACCESS_KEY_ID" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "file": { - "description": "Repository-relative encrypted credential file staged through the trusted secret flow. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "secrets/backup.env" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "default": "sops", - "description": "Trusted secret provider. Only sops is currently executable.", - "enum": [ - "sops" - ], - "type": "string" - }, - "secret_key_entry": { - "description": "Variable name containing the destination secret key. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "BACKUP_SECRET_ACCESS_KEY" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "session_token_entry": { - "description": "Optional variable name containing a temporary destination session token. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "BACKUP_SESSION_TOKEN" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - } - }, - "type": "object" - }, - "encryption": { - "additionalProperties": false, - "description": "Required encryption mode for each recovery kind this target may store.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cold": { - "description": "Encryption mode required for cold recovery: client-side or server-side.", - "enum": [ - "client-side", - "server-side" - ], - "type": "string" - }, - "pitr": { - "description": "Encryption mode required for point-in-time recovery: client-side or server-side.", - "enum": [ - "client-side", - "server-side" - ], - "type": "string" - }, - "snapshot": { - "description": "Encryption mode required for snapshot recovery: client-side or server-side.", - "enum": [ - "client-side", - "server-side" - ], - "type": "string" - } - }, - "type": "object" - }, - "endpoint": { - "description": "Destination API endpoint. HTTPS is required unless tls is explicitly insecure. Expects an http or https URL.", - "examples": [ - "https://objects.example.com" - ], - "pattern": "^https?://", - "type": "string" - }, - "failure_domain": { - "additionalProperties": false, - "description": "Operator-declared identity used to prove the destination does not share the protected host.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "host": { - "description": "Destination host identity used to refuse a target on the protected host. Expects a stable identifier of letters, digits, dots, colons, slashes, underscores and hyphens.", - "examples": [ - "backup-01.example.net" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$", - "type": "string" - }, - "identity": { - "description": "Stable operator-owned failure-domain identity, distinct from the protected host. Expects a stable identifier of letters, digits, dots, colons, slashes, underscores and hyphens.", - "examples": [ - "provider-a/us-east-1/account-42" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$", - "type": "string" - } - }, - "type": "object" - }, - "kind": { - "description": "Destination kind. Only s3-compatible is supported.", - "enum": [ - "s3-compatible" - ], - "examples": [ - "s3-compatible" - ], - "type": "string" - }, - "prefix": { - "description": "Non-secret object prefix reserved for Onebox backup data. Expects a relative object prefix with no empty leading component or shell metacharacter.", - "examples": [ - "production/shop" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,511}$", - "type": "string" - }, - "region": { - "description": "S3-compatible region when the endpoint requires one. Expects a lower-case S3-compatible region of letters, digits and hyphens.", - "examples": [ - "us-east-1" - ], - "pattern": "^[a-z0-9][a-z0-9-]{0,62}$", - "type": "string" - }, - "tls": { - "default": "verify", - "description": "Transport policy: verify, or skip-verify to accept a plaintext http endpoint.", - "enum": [ - "verify", - "skip-verify" - ], - "type": "string" - } - }, - "type": "object" - }, - "description": "User-owned off-host repositories available to service backup policies.", - "type": "object" - }, - "base_path": { - "default": "/var/lib/ob", - "description": "Absolute host directory beneath which Onebox stores application state and releases. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/srv/ob" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "build": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Build metadata for development. Production requires a resolved image supplied with --image.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "args": { - "additionalProperties": {}, - "description": "Build arguments supplied by the external build system.", - "type": "object" - }, - "context": { - "description": "Repository-relative build context. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "." - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "dockerfile": { - "description": "Repository-relative Dockerfile path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "Dockerfile" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "target": { - "description": "Named Dockerfile stage to build.", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Build metadata for development. Production requires a resolved image supplied with --image. Also accepts a build context path." - }, - "checks": { - "additionalProperties": false, - "description": "Assertions that must pass before a release becomes current unless marked advisory.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "exec": { - "description": "Commands run inside a named workload.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "run": { - "description": "Shell command verified inside the workload.", - "examples": [ - "test -f /srv/ready" - ], - "type": "string" - }, - "workload": { - "description": "Workload the command runs inside.", - "examples": [ - "web" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "http": { - "description": "HTTP paths probed inside a named workload.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "path": { - "description": "HTTP path verified inside the workload. Expects a path beginning with /.", - "examples": [ - "/healthz" - ], - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "port": { - "description": "Container port to probe.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "workload": { - "description": "Workload the path is probed inside.", - "examples": [ - "web" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "migrations": { - "description": "Migration revisions checked against captured job evidence.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "applied_revisions": { - "description": "Revisions the job must report as applied.", - "items": { - "type": "string" - }, - "type": "array" - }, - "job": { - "description": "Job workload whose captured evidence is checked.", - "examples": [ - "migrate" - ], - "type": "string" - }, - "provider": { - "description": "Migration tool that produced the revisions.", - "examples": [ - "alembic" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "url": { - "description": "External URLs probed from the operator side.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "advisory": { - "default": false, - "description": "Report a failure without blocking release activation.", - "type": "boolean" - }, - "contains": { - "description": "Text the response body must contain.", - "type": "string" - }, - "json_assertions": { - "description": "Scalar JSON response values that must match exactly.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "equals": { - "description": "Exact scalar value required at path." - }, - "path": { - "description": "Dot-separated path to a scalar value in the JSON response.", - "examples": [ - "service.ready" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "required_headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Exact response headers required for success.", - "type": "object" - }, - "status_codes": { - "description": "Allowed response status codes. A successful 2xx response is expected when omitted.", - "items": { - "maximum": 599, - "minimum": 100, - "type": "integer" - }, - "type": "array" - }, - "url": { - "description": "External HTTP or HTTPS URL verified from the operator side. Expects an http or https URL.", - "examples": [ - "https://shop.example.com/healthz" - ], - "pattern": "^https?://", - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - }, - "compose": { - "description": "Existing Compose service to adopt, as repository path#service. Expects a reference of the form path/to/compose.yaml#service.", - "examples": [ - "docker-compose.yml#web" - ], - "pattern": "^[^/#][^#]*#[a-zA-Z0-9._-]+$", - "type": "string" - }, - "deployment": { - "additionalProperties": false, - "description": "Release ordering, retention, and migration behavior.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "migration_policy": { - "default": "manual", - "description": "Policy for migration jobs during release and recovery.", - "enum": [ - "manual", - "auto", - "expand-only" - ], - "type": "string" - }, - "order": { - "description": "Explicit workload release order. Dependency order is derived when omitted.", - "items": { - "type": "string" - }, - "type": "array" - }, - "retain_releases": { - "default": 5, - "description": "Number of completed release directories retained for inspection and rollback.", - "minimum": 1, - "type": "integer" - } - }, - "type": "object" - }, - "environments": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "base_path": { - "description": "Environment-specific replacement for the project base_path. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/srv/ob" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "env_files": { - "description": "Default ordered environment-file list for application, worker, and job workloads in this environment.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative environment file path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "description": "Decryptor used before staging the file. The supported encrypted provider is sops.", - "enum": [ - "sops" - ], - "examples": [ - "sops" - ], - "type": "string" - } - }, - "required": [ - "file" - ], - "type": "object" - } - ], - "description": "Also accepts a path to an environment file." - }, - "type": "array" - }, - "jump": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Optional SSH jump host tunnelling the connection to this server, written as user@host or as an object with host, user, and port. Onebox verifies and authenticates both hops and never forwards the SSH agent.", - "examples": [ - "deploy@bastion.example.com" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "host": { - "description": "Jump host name or IP address.", - "examples": [ - "bastion.example.com" - ], - "type": "string" - }, - "port": { - "description": "SSH port on the jump host. The SSH default is used when omitted.", - "examples": [ - 2222 - ], - "type": "integer" - }, - "user": { - "description": "SSH user on the jump host. $USER is used when omitted; ob does not read ~/.ssh/config.", - "examples": [ - "deploy" - ], - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Optional SSH jump host tunnelling the connection to this server, written as user@host or as an object with host, user, and port. Onebox verifies and authenticates both hops and never forwards the SSH agent. Also accepts user@host or user@host:port." - }, - "overrides": { - "additionalProperties": false, - "description": "Environment-specific operational tuning. Overrides cannot change workload identity or data semantics.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "services": { - "additionalProperties": { - "additionalProperties": {}, - "type": "object" - }, - "description": "Allowed service tuning keyed by service name: resources and settings.", - "type": "object" - }, - "workloads": { - "additionalProperties": { - "additionalProperties": {}, - "type": "object" - }, - "description": "Allowed workload tuning keyed by workload name: replicas, resources, env, env_files, strategy, and routes.", - "type": "object" - } - }, - "type": "object" - }, - "policy": { - "additionalProperties": false, - "description": "Approval, runner compatibility, and migration-backup requirements for this environment.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "allow_agent_proposals": { - "default": true, - "description": "Declared permission for agent-authored proposals. The current CLI does not distinguish agent identity; execution remains approval-gated.", - "type": "boolean" - }, - "migrations": { - "additionalProperties": false, - "description": "What this environment requires of a release carrying migration risk.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "backup_key_material": { - "description": "Key-material identities the backup report must name.", - "examples": [ - [ - "BACKUP_ACCESS_KEY_ID" - ] - ], - "items": { - "type": "string" - }, - "type": "array" - }, - "backup_max_age": { - "default": "24h", - "description": "Maximum age of a backup report accepted for a migration. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "24h" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "require_backup": { - "default": false, - "description": "Require a plan-bound backup report before a release with migration risk.", - "type": "boolean" - }, - "require_restore_test": { - "default": false, - "description": "Require the backup report to state that a restore test succeeded.", - "type": "boolean" - } - }, - "type": "object" - }, - "min_onebox_version": { - "description": "Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.8.0.", - "examples": [ - "v2026.8.0" - ], - "pattern": "^v([1-9][0-9]{3})\\.([1-9]|1[0-2])\\.(0|[1-9][0-9]{0,18})$", - "type": "string" - }, - "min_plan_schema": { - "description": "Oldest executable plan schema accepted by this environment. Expects a plan schema identity such as onebox.run/executable-deploy-plan/v1alpha2.", - "examples": [ - "onebox.run/executable-deploy-plan/v1alpha2" - ], - "pattern": "^onebox\\.run/executable-deploy-plan/v[1-9][0-9]*((alpha|beta)[1-9][0-9]*)?$", - "type": "string" - }, - "require_approval": { - "default": true, - "description": "Require a plan-bound local confirmation before mutating this environment.", - "type": "boolean" - } - }, - "type": "object" - }, - "server": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "SSH server, written as user@host or as an object with host, user, and port.", - "examples": [ - "root@203.0.113.10" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "host": { - "description": "SSH hostname or IP address.", - "examples": [ - "203.0.113.10" - ], - "type": "string" - }, - "port": { - "description": "SSH port. The SSH default is used when omitted.", - "examples": [ - 2222 - ], - "type": "integer" - }, - "user": { - "description": "SSH user. $USER is used when omitted; ob does not read ~/.ssh/config.", - "examples": [ - "root" - ], - "type": "string" - } - }, - "type": "object" - } - ], - "description": "SSH server, written as user@host or as an object with host, user, and port. Also accepts user@host." - } - }, - "type": "object" - }, - "description": "Named environments, each naming the server it deploys to and the policy applied to it.", - "minProperties": 1, - "type": "object" - }, - "external_services": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "backup_owner": { - "description": "Operator or provider responsible for backup, restore, upgrades, credentials, and durability. Expects a stable operator or provider identity of letters, digits, dots, @, colons, slashes, underscores and hyphens.", - "examples": [ - "platform-team/rds" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9._@:/-]{0,127}$", - "type": "string" - }, - "connection": { - "additionalProperties": false, - "description": "Trusted connection source and driver-shaped entry mapping.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "entries": { - "additionalProperties": { - "description": "Expects a variable name of letters, digits and underscores, not starting with a digit.", - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "description": "Maps driver connection parts such as host, port, user, password, database, or url to variable names in the trusted source.", - "type": "object" - }, - "source": { - "additionalProperties": false, - "description": "Trusted encrypted file containing the connection values.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative encrypted environment file staged through the trusted secret flow. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "secrets/production-db.env" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "default": "sops", - "description": "Trusted secret provider. Only sops is currently executable.", - "enum": [ - "sops" - ], - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "driver": { - "description": "Built-in connection shape used to validate and project this dependency.", - "enum": [ - "clickhouse", - "mariadb", - "meilisearch", - "minio", - "mongodb", - "mysql", - "nats", - "postgres", - "rabbitmq", - "redis", - "valkey" - ], - "examples": [ - "postgres" - ], - "type": "string" - }, - "probe": { - "additionalProperties": false, - "description": "Optional bounded read-only health observation; it never creates or repairs provider resources.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "kind": { - "default": "driver-health", - "description": "Read-only observation kind: driver-health.", - "enum": [ - "driver-health" - ], - "type": "string" - }, - "max_age": { - "default": "5m", - "description": "Maximum age of a probe observation bound into a plan. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "timeout": { - "default": "5s", - "description": "Maximum duration of one read-only probe. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "description": "Typed dependencies operated outside Onebox. Their connection projection is trusted, but their lifecycle and backup remain external.", - "type": "object" - }, - "health": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Readiness check used to gate rolling replacement.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "exec": { - "description": "Health command as a shell string or direct argument list." - }, - "http": { - "description": "HTTP path probed inside the container. Expects a path beginning with /.", - "examples": [ - "/healthz" - ], - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "interval": { - "default": "5s", - "description": "Delay between container health probes, at most 7d. Always written into the generated healthcheck, so the rollout's drain budget is computed from the value the container actually runs with. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "2s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "port": { - "description": "Container port probed by HTTP or TCP health checks.", - "examples": [ - 8080 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "retries": { - "default": 3, - "description": "Consecutive failed probes before the container is unhealthy. A draining container leaves rotation after this many probes, so it sets how long a rolling deploy waits for each replica.", - "examples": [ - 3 - ], - "type": "integer" - }, - "start_period": { - "default": "30s", - "description": "Startup grace period before failed probes count, at most 7d. Always written into the generated healthcheck, so writing down a fast probe interval does not call a booting container unhealthy. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "tcp": { - "default": false, - "description": "Probe the configured port by opening a TCP connection.", - "type": "boolean" - }, - "within": { - "description": "Maximum time a rollout waits for readiness, at most 7d. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "120s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Readiness check used to gate rolling replacement. Also accepts an HTTP health path." - }, - "hooks": { - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "local": { - "default": false, - "description": "Run on the operator machine instead of the server.", - "type": "boolean" - }, - "run": { - "description": "Command executed at the lifecycle seam.", - "examples": [ - "./scripts/notify.sh" - ], - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Also accepts the command to run." - }, - "description": "Lifecycle commands keyed by seam: bootstrap, pre_release, post_release, or post_deploy.", - "type": "object" - }, - "image": { - "anyOf": [ - { - "description": "Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - }, - { - "additionalProperties": false, - "description": "Container image source, written as a reference string or an object.", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "pull": { - "default": "missing", - "description": "When to fetch the image from the registry: missing fetches only what the host does not already hold, always fetches every release, never fetches at all and fails on a missing image.", - "enum": [ - "always", - "missing", - "never" - ], - "type": "string" - }, - "reference": { - "description": "Complete container image reference, optionally tagged or digest-pinned. Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Container image source, written as a reference string or an object. Also accepts an image reference." - }, - "notifications": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "format": { - "default": "text", - "description": "Notification payload format.", - "enum": [ - "text", - "json" - ], - "type": "string" - }, - "on": { - "default": [ - "success", - "failure" - ], - "description": "Operation outcomes that trigger this notification.", - "items": { - "enum": [ - "success", - "failure" - ], - "type": "string" - }, - "type": "array" - }, - "webhook": { - "description": "HTTP endpoint that receives outcome notifications.", - "examples": [ - "https://hooks.example.com/onebox" - ], - "type": "string" - } - }, - "type": "object" - }, - "description": "Named webhooks that receive selected operation and scheduled-job outcomes.", - "type": "object" - }, - "port": { - "description": "Default container port used by HTTP health checks.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "proxy": { - "additionalProperties": false, - "description": "Ownership and configuration of the host ingress proxy.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "config": { - "description": "Repository-relative proxy configuration directory. Dynamic YAML or TOML files extend Onebox's managed configuration. A managed DNS challenge may use a directory containing only .env for provider credentials. Including traefik.yml or traefik.yaml instead takes ownership of the static configuration, which must use the watched file-provider directory /etc/traefik/dynamic, must not enable the Docker provider, must define certificatesResolvers.letsencrypt for exact terminating routes, and must define the DNS-01 certificatesResolvers.onebox-wildcard for wildcard terminating routes. Dynamic files may not reuse Onebox-generated router or service names or redefine the managed onebox-compress middleware. Expects a path inside the repository, with no control character or shell metacharacter.", - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "dns_challenge": { - "additionalProperties": false, - "description": "Managed ACME DNS-01 challenge used to issue wildcard certificates. Provider credentials belong in proxy.config/.env; Onebox continues to own the static proxy configuration.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "provider": { - "description": "Traefik DNS challenge provider name. Its credential variables must be supplied through proxy.config/.env. Expects a lower-case Traefik DNS provider name such as cloudflare or route53.", - "examples": [ - "cloudflare" - ], - "pattern": "^[a-z][a-z0-9_-]*$", - "type": "string" - }, - "resolvers": { - "description": "DNS resolvers used to verify challenge propagation, written as host:port.", - "examples": [ - [ - "1.1.1.1:53" - ] - ], - "items": { - "description": "Expects a lower-case DNS name, IPv4 address, or bracketed IPv6 address followed by a port.", - "pattern": "^([a-z0-9]([a-z0-9.-]*[a-z0-9])?|\\[[0-9A-Fa-f:.]+\\]):[0-9]{1,5}$", - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "provider" - ], - "type": "object" - }, - "entrypoints": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "port": { - "description": "Host and proxy-container TCP port used by this listener.", - "examples": [ - 4317 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - } - }, - "type": "object" - }, - "description": "Additional named TCP listeners published by the managed proxy. Onebox adds them to its generated static configuration; a proxy.config containing custom traefik.yml or traefik.yaml must define matching Traefik entrypoints.", - "propertyNames": { - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$" - }, - "type": "object" - }, - "image": { - "description": "Container image used for the managed proxy. Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - }, - "kind": { - "default": "traefik-docker", - "description": "Proxy implementation, or none to disable routing.", - "enum": [ - "traefik-docker", - "none" - ], - "type": "string" - }, - "managed": { - "description": "Let Onebox converge the host-scoped proxy when routes are declared.", - "type": "boolean" - }, - "network": { - "default": "ob-ingress", - "description": "External container network shared with routed workloads; default and Onebox's derived application and service network names are reserved.", - "type": "string" - } - }, - "type": "object" - }, - "registries": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "password_env": { - "description": "Local environment-variable name containing the registry password or token. Expects a variable name of letters, digits and underscores, not starting with a digit.", - "examples": [ - "GHCR_TOKEN" - ], - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", - "type": "string" - }, - "server": { - "description": "Registry hostname, optionally with a port. Expects a host with an optional port and path, such as ghcr.io or registry.example.com:5000.", - "examples": [ - "ghcr.io" - ], - "pattern": "^[A-Za-z0-9][A-Za-z0-9.-]*(:[0-9]{1,5})?(/[A-Za-z0-9._/-]*)?$", - "type": "string" - }, - "username": { - "description": "Registry login username. Expects a username of letters, digits and . _ @ + -.", - "pattern": "^[A-Za-z0-9][A-Za-z0-9._@+-]*$", - "type": "string" - } - }, - "type": "object" - }, - "description": "Named container registries and the environment variables holding their credentials.", - "type": "object" - }, - "routes": { - "description": "Ingress routes exposed by this workload.", - "items": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "hostname": { - "const": "*" - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - }, - "tls": { - "enum": [ - "none", - "passthrough" - ] - } - }, - "required": [ - "protocol", - "tls" - ] - } - }, - { - "if": { - "properties": { - "hostname": { - "pattern": "^\\*\\." - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "http" - } - } - } - }, - { - "if": { - "properties": { - "tls": { - "const": "passthrough" - } - }, - "required": [ - "tls" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - } - }, - "required": [ - "protocol" - ] - } - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, - "hostname": { - "anyOf": [ - { - "maxLength": 253, - "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" - }, - { - "const": "*" - } - ], - "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", - "examples": [ - "shop.example.com" - ], - "type": "string" - }, - "middlewares": { - "description": "Ordered provider-qualified middleware references applied to this route.", - "items": { - "description": "Expects a provider-qualified name such as secure-headers@file.", - "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*@[a-z][a-z0-9-]*$", - "type": "string" - }, - "type": "array" - }, - "path": { - "default": "/", - "description": "URL path prefix matched by an HTTP route. Expects a path beginning with /.", - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "port": { - "description": "Container port receiving routed traffic.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "protocol": { - "default": "http", - "description": "Routing protocol: http or tcp.", - "enum": [ - "http", - "tcp" - ], - "type": "string" - }, - "scheme": { - "default": "http", - "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", - "enum": [ - "http", - "https", - "h2c" - ], - "type": "string" - }, - "tls": { - "default": "terminate", - "description": "TLS handling: terminate, passthrough, or none.", - "enum": [ - "terminate", - "passthrough", - "none" - ], - "type": "string" - } - }, - "required": [ - "hostname" - ], - "type": "object" - }, - "type": "array" - }, - "runtime": { - "additionalProperties": false, - "description": "Project-wide environment files and local environment-file requirements.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "env_checks": { - "description": "Local environment-file assertions checked before planning or deploying.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative dotenv file whose declared keys are checked. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "present": { - "description": "Environment keys that must be declared but may be empty.", - "items": { - "type": "string" - }, - "type": "array" - }, - "require": { - "description": "Environment keys that must be declared with non-empty values.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "type": "array" - }, - "env_files": { - "description": "Project-wide ordered environment-file list for application, worker, and job workloads.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative environment file path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "description": "Decryptor used before staging the file. The supported encrypted provider is sops.", - "enum": [ - "sops" - ], - "examples": [ - "sops" - ], - "type": "string" - } - }, - "required": [ - "file" - ], - "type": "object" - } - ], - "description": "Also accepts a path to an environment file." - }, - "type": "array" - } - }, - "type": "object" - }, - "services": { - "additionalProperties": { - "anyOf": [ - { - "type": [ - "string", - "number", - "integer" - ] - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "backup": { - "additionalProperties": false, - "description": "Recovery intent for this service. Onebox selects the qualified native implementation; declaring intent alone does not establish backup.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "allow_downtime": { - "default": false, - "description": "Whether recurring backup operations may use the driver-declared stopped-service window.", - "type": "boolean" - }, - "drill": { - "additionalProperties": false, - "description": "Exact isolated restore-test schedule, proof age, and optional staging filesystem.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "max_age": { - "default": "7d", - "description": "Maximum age of the latest passing restore proof. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "7d" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "schedule": { - "additionalProperties": false, - "description": "Exact recurring isolated restore-test schedule.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cron": { - "description": "Five-field cron schedule translated to a host timer. Expects five cron fields.", - "examples": [ - "0 2 * * *" - ], - "pattern": "^[-0-9*/,A-Za-z ]+$", - "type": "string" - }, - "timezone": { - "default": "UTC", - "description": "IANA timezone used to interpret the cron schedule. Expects an IANA zone name such as UTC or Europe/Berlin.", - "examples": [ - "Europe/Berlin" - ], - "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$", - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "max_data_loss": { - "description": "Maximum tolerable interval between the latest recoverable point and failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "15m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "recovery_kind": { - "description": "Required recovery envelope: snapshot, pitr, or cold.", - "enum": [ - "snapshot", - "pitr", - "cold" - ], - "examples": [ - "pitr" - ], - "type": "string" - }, - "retention": { - "additionalProperties": false, - "description": "Portable minimum recovery history that the selected native driver must be able to preserve.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "keep": { - "default": 7, - "description": "Minimum number of independently recoverable base generations to retain.", - "examples": [ - 7 - ], - "minimum": 1, - "type": "integer" - }, - "window": { - "default": "7d", - "description": "Minimum continuous recovery history the native retention mapping must preserve. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "7d" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - }, - "schedule": { - "additionalProperties": false, - "description": "Exact recurring base-backup schedule.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cron": { - "description": "Five-field cron schedule translated to a host timer. Expects five cron fields.", - "examples": [ - "0 2 * * *" - ], - "pattern": "^[-0-9*/,A-Za-z ]+$", - "type": "string" - }, - "timezone": { - "default": "UTC", - "description": "IANA timezone used to interpret the cron schedule. Expects an IANA zone name such as UTC or Europe/Berlin.", - "examples": [ - "Europe/Berlin" - ], - "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$", - "type": "string" - } - }, - "type": "object" - }, - "target": { - "description": "Name of a project-level backup target. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "examples": [ - "offsite" - ], - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - } - }, - "type": "object" - }, - "driver": { - "description": "Built-in service driver. Defaults to the service map key. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "examples": [ - "postgres" - ], - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "features": { - "additionalProperties": false, - "description": "Capabilities Onebox must establish before application workloads run.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "extensions": { - "additionalProperties": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": {}, - "type": "object" - }, - "description": "PostgreSQL extensions Onebox installs in the managed application database before application migrations run.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9_-]*$" - }, - "type": "object" - } - }, - "type": "object" - }, - "persistence": { - "additionalProperties": false, - "description": "Data-lifetime declaration for this supporting service.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "mode": { - "default": "durable", - "description": "Data lifetime: durable, ephemeral, or external.", - "enum": [ - "durable", - "ephemeral", - "external" - ], - "type": "string" - } - }, - "type": "object" - }, - "resources": { - "additionalProperties": false, - "description": "Memory and CPU limits for this supporting service.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cpus": { - "description": "Container CPU limit expressed as a positive decimal count. Expects a number of CPUs such as 0.5 or 2.", - "examples": [ - "0.5" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?$", - "type": "string" - }, - "memory": { - "description": "Container memory limit. Expects a size such as 512MB or 1.5GB.", - "examples": [ - "512MB" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?(B|KB|MB|GB|TB)$", - "type": "string" - } - }, - "type": "object" - }, - "settings": { - "additionalProperties": {}, - "description": "Driver-specific settings validated by the selected service driver.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9_-]*$" - }, - "type": "object" - }, - "version": { - "description": "Driver version or image tag to run.", - "examples": [ - "17" - ] - }, - "volumes": { - "description": "Additional driver-defined persistent volume names.", - "items": { - "description": "Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - ], - "description": "Also accepts the version to run." - }, - "description": "Supporting services managed outside application releases, such as databases and caches.", - "type": "object" - }, - "workloads": { - "additionalProperties": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "required": [ - "execution" - ] - }, - "then": { - "not": { - "required": [ - "compose" - ] - }, - "properties": { - "data_effect": { - "const": "none" - }, - "deployment_phase": { - "const": "none" - }, - "operator_run": { - "const": "allowed" - } - }, - "required": [ - "schedule", - "data_effect" - ] - } - }, - { - "oneOf": [ - { - "required": [ - "build" - ] - }, - { - "required": [ - "image" - ] - }, - { - "required": [ - "compose" - ] - } - ] - }, - { - "not": { - "allOf": [ - { - "required": [ - "published_ports" - ] - }, - { - "anyOf": [ - { - "properties": { - "strategy": { - "const": "rolling" - } - }, - "required": [ - "strategy" - ] - }, - { - "allOf": [ - { - "not": { - "required": [ - "strategy" - ] - } - }, - { - "required": [ - "health" - ] - }, - { - "anyOf": [ - { - "properties": { - "role": { - "const": "application" - } - }, - "required": [ - "role" - ] - }, - { - "not": { - "required": [ - "role" - ] - } - } - ] - } - ] - } - ] - } - ] - } - }, - { - "if": { - "properties": { - "persistence": { - "anyOf": [ - { - "properties": { - "mode": { - "const": "durable" - } - }, - "required": [ - "mode" - ] - }, - { - "not": { - "required": [ - "mode" - ] - } - } - ] - } - }, - "required": [ - "persistence" - ] - }, - "then": { - "properties": { - "replicas": { - "maximum": 1 - } - } - } - }, - { - "else": { - "not": { - "anyOf": [ - { - "required": [ - "deployment_phase" - ] - }, - { - "required": [ - "operator_run" - ] - }, - { - "required": [ - "data_effect" - ] - }, - { - "required": [ - "schedule" - ] - }, - { - "required": [ - "inputs" - ] - }, - { - "required": [ - "execution" - ] - } - ] - } - }, - "if": { - "properties": { - "role": { - "const": "job" - } - }, - "required": [ - "role" - ] - }, - "then": { - "required": [ - "data_effect" - ] - } - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "build": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Build metadata for development. Production requires a resolved image supplied with --image.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "args": { - "additionalProperties": {}, - "description": "Build arguments supplied by the external build system.", - "type": "object" - }, - "context": { - "description": "Repository-relative build context. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "." - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "dockerfile": { - "description": "Repository-relative Dockerfile path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - "Dockerfile" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "target": { - "description": "Named Dockerfile stage to build.", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Build metadata for development. Production requires a resolved image supplied with --image. Also accepts a build context path." - }, - "command": { - "anyOf": [ - { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, - { - "description": "Container command as a shell string or argument list.", - "examples": [ - "./bin/server" - ] - } - ], - "description": "Container command as a shell string or argument list. Also accepts a command line or argument list." - }, - "compose": { - "description": "Existing Compose service to adopt, as repository path#service. Expects a reference of the form path/to/compose.yaml#service.", - "examples": [ - "docker-compose.yml#web" - ], - "pattern": "^[^/#][^#]*#[a-zA-Z0-9._-]+$", - "type": "string" - }, - "data_effect": { - "description": "Job data impact used by rollback and abort gates.", - "enum": [ - "none", - "migration", - "destructive", - "unknown" - ], - "examples": [ - "migration" - ], - "type": "string" - }, - "deployment_phase": { - "default": "none", - "description": "Deployment phase for this job: none, pre_release, or post_release.", - "enum": [ - "none", - "pre_release", - "post_release" - ], - "type": "string" - }, - "drain": { - "additionalProperties": false, - "description": "Signal and timing used to remove a container from traffic before stopping it.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "grace": { - "description": "Maximum graceful-shutdown time before forced termination, at most 7d. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "signal": { - "default": "TERM", - "description": "Signal sent to begin graceful shutdown. Expects a signal name such as TERM or QUIT.", - "pattern": "^[A-Z][A-Z0-9]*$", - "type": "string" - }, - "wait": { - "description": "Maximum drain window before shutdown continues, at most 7d. Recreate workloads continue sooner when every old container exits. Rolling workloads wait the full interval before stopping each container when their health check supports drain guarding. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "10s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - }, - "entrypoint": { - "anyOf": [ - { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, - { - "description": "Container entrypoint as a string or argument list." - } - ], - "description": "Container entrypoint as a string or argument list. Also accepts an entrypoint or argument list." - }, - "env": { - "additionalProperties": {}, - "description": "Literal container environment values. Managed-service credential variables cannot be overridden.", - "type": "object" - }, - "env_files": { - "description": "Workload-specific ordered environment-file list. Replaces broader defaults when present.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "file": { - "description": "Repository-relative environment file path. Expects a path inside the repository, with no control character or shell metacharacter.", - "examples": [ - ".env.production" - ], - "pattern": "^[^/\\x00-\\x1f'\"$`\\\\][^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "provider": { - "description": "Decryptor used before staging the file. The supported encrypted provider is sops.", - "enum": [ - "sops" - ], - "examples": [ - "sops" - ], - "type": "string" - } - }, - "required": [ - "file" - ], - "type": "object" - } - ], - "description": "Also accepts a path to an environment file." - }, - "type": "array" - }, - "execution": { - "additionalProperties": false, - "description": "Opt-in durable scheduled execution. Requires a native operator-runnable phase-none job with data_effect none. Stores non-secret checkpoints on the host and permits explicit same-release resume.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "retention": { - "default": "168h", - "description": "Time from creation during which an unsuccessful execution may be resumed, at most 30d. Active executions remain protected. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "steps": { - "description": "Optional ordered steps using this job's image and entrypoint. Omit to execute the job command as one step. At most 32 steps.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "command": { - "description": "Argument vector passed to the job image's entrypoint. No shell evaluation is performed.", - "items": { - "type": "string" - }, - "maxItems": 128, - "minItems": 1, - "type": "array" - }, - "id": { - "description": "Unique stable step identifier, used by output references. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "inputs": { - "additionalProperties": { - "type": "string" - }, - "description": "Environment variables populated from a preceding step's declared output, written as step.OUTPUT.", - "propertyNames": { - "pattern": "^[A-Z][A-Z0-9_]*$" - }, - "type": "object" - }, - "outputs": { - "description": "Required string keys in the JSON object written to ONEBOX_OUTPUT_FILE. Values are non-secret, at most 4096 bytes each and 16384 bytes total.", - "items": { - "description": "Expects upper-case letters, digits and underscores, starting with a letter.", - "pattern": "^[A-Z][A-Z0-9_]*$", - "type": "string" - }, - "maxItems": 32, - "type": "array", - "uniqueItems": true - }, - "retry": { - "additionalProperties": false, - "description": "Per-step retry policy; defaults to schedule.retry. All steps and backoff share the activation timeout.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "attempts": { - "default": 1, - "description": "Total attempts including the first, 1 to 10.", - "examples": [ - 3 - ], - "maximum": 10, - "minimum": 1, - "type": "integer" - }, - "backoff": { - "default": "30s", - "description": "Sleep before the second attempt; it doubles after each failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "1m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "max_backoff": { - "default": "10m", - "description": "Upper bound for the doubling sleep. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - }, - "required": [ - "id", - "command" - ], - "type": "object" - }, - "maxItems": 32, - "type": "array" - } - }, - "type": "object" - }, - "extra_hosts": { - "description": "Additional host-to-address entries added to the container.", - "items": { - "type": "string" - }, - "type": "array" - }, - "health": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "description": "Readiness check used to gate rolling replacement.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "exec": { - "description": "Health command as a shell string or direct argument list." - }, - "http": { - "description": "HTTP path probed inside the container. Expects a path beginning with /.", - "examples": [ - "/healthz" - ], - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "interval": { - "default": "5s", - "description": "Delay between container health probes, at most 7d. Always written into the generated healthcheck, so the rollout's drain budget is computed from the value the container actually runs with. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "2s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "port": { - "description": "Container port probed by HTTP or TCP health checks.", - "examples": [ - 8080 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "retries": { - "default": 3, - "description": "Consecutive failed probes before the container is unhealthy. A draining container leaves rotation after this many probes, so it sets how long a rolling deploy waits for each replica.", - "examples": [ - 3 - ], - "type": "integer" - }, - "start_period": { - "default": "30s", - "description": "Startup grace period before failed probes count, at most 7d. Always written into the generated healthcheck, so writing down a fast probe interval does not call a booting container unhealthy. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "5s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "tcp": { - "default": false, - "description": "Probe the configured port by opening a TCP connection.", - "type": "boolean" - }, - "within": { - "description": "Maximum time a rollout waits for readiness, at most 7d. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "120s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Readiness check used to gate rolling replacement. Also accepts an HTTP health path." - }, - "hostname": { - "description": "Hostname assigned inside the workload container.", - "type": "string" - }, - "image": { - "anyOf": [ - { - "description": "Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - }, - { - "additionalProperties": false, - "description": "Container image source, written as a reference string or an object.", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "pull": { - "default": "missing", - "description": "When to fetch the image from the registry: missing fetches only what the host does not already hold, always fetches every release, never fetches at all and fails on a missing image.", - "enum": [ - "always", - "missing", - "never" - ], - "type": "string" - }, - "reference": { - "description": "Complete container image reference, optionally tagged or digest-pinned. Expects a registry reference such as nginx:1.27 or ghcr.io/acme/app@sha256:….", - "examples": [ - "ghcr.io/acme/shop:1.4.0" - ], - "pattern": "^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[(?:[a-fA-F0-9:]+)\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Container image source, written as a reference string or an object. Also accepts an image reference." - }, - "init": { - "description": "Run a minimal init process as PID 1 inside the container.", - "type": "boolean" - }, - "inputs": { - "additionalProperties": { - "additionalProperties": false, - "oneOf": [ - { - "required": [ - "enum" - ] - }, - { - "required": [ - "pattern" - ] - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "default": { - "description": "Value used by a timer firing and by an operator run that does not override it. Must satisfy the input's own constraint.", - "type": "string" - }, - "description": { - "description": "What the input controls.", - "type": "string" - }, - "enum": { - "description": "Accepted values.", - "examples": [ - [ - "catalog" - ] - ], - "items": { - "type": "string" - }, - "type": "array" - }, - "pattern": { - "description": "Regular expression the whole value must match.", - "examples": [ - "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" - ], - "type": "string" - } - }, - "required": [ - "default" - ], - "type": "object" - }, - "description": "Declared parameters of a scheduled job, exposed as environment variables. Names are upper-case identifiers; each declares exactly one of enum or pattern and a default. A timer firing uses the defaults; ob job run may override them.", - "propertyNames": { - "pattern": "^[A-Z][A-Z0-9_]*$" - }, - "type": "object" - }, - "labels": { - "additionalProperties": {}, - "description": "Additional container labels outside namespaces reserved by Onebox and the proxy.", - "type": "object" - }, - "logging": { - "additionalProperties": false, - "description": "Container logging driver and driver-specific options.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "driver": { - "description": "Container runtime logging driver. Expects a log driver name such as local, json-file or an org/plugin:tag.", - "examples": [ - "local" - ], - "pattern": "^[a-z0-9][a-z0-9_.-]*(/[a-z0-9][a-z0-9_.-]*)?(:[A-Za-z0-9_.-]+)?$", - "type": "string" - }, - "options": { - "additionalProperties": {}, - "description": "Driver-specific logging options passed to the container runtime.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9_.-]*$" - }, - "type": "object" - } - }, - "type": "object" - }, - "needs": { - "description": "Workload or supporting-service prerequisites and optional connection-variable mappings.", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "condition": { - "description": "Prerequisite condition: started, healthy, or completed.", - "enum": [ - "started", - "healthy", - "completed" - ], - "type": "string" - }, - "env": { - "additionalProperties": { - "type": "string" - }, - "description": "Maps application environment-variable names to service connection parts such as host, port, user, password, database, or url.", - "type": "object" - }, - "name": { - "description": "Name of a workload or supporting service that must start first. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - } - }, - "type": "object" - } - ], - "description": "Also accepts the name of a prerequisite." - }, - "type": "array" - }, - "operator_run": { - "description": "Whether an operator may invoke this job outside deployment: allowed or disabled. Defaults to allowed for phase none and disabled otherwise.", - "enum": [ - "allowed", - "disabled" - ], - "type": "string" - }, - "persistence": { - "additionalProperties": false, - "description": "Declares whether this workload holds data that must outlive releases.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "mode": { - "default": "durable", - "description": "Data lifetime: durable, ephemeral, or external.", - "enum": [ - "durable", - "ephemeral", - "external" - ], - "type": "string" - } - }, - "type": "object" - }, - "port": { - "description": "Default container port used by HTTP health checks.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "published_ports": { - "description": "Host ports published outside the proxy. They bind to loopback by default. A rolling workload cannot publish one, because two replicas cannot hold the same host port during a roll: set strategy: recreate, or route through the proxy instead.", - "items": { - "additionalProperties": false, - "patternProperties": { - "^x-": {} - }, - "properties": { - "bind": { - "default": "127.0.0.1", - "description": "Host address on which the published port listens.", - "type": "string" - }, - "container": { - "description": "Port receiving traffic inside the container.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "host": { - "description": "Port exposed on the host.", - "examples": [ - 8080 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "protocol": { - "default": "tcp", - "description": "Published transport protocol: tcp or udp.", - "enum": [ - "tcp", - "udp" - ], - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "replicas": { - "default": 1, - "description": "Desired number of long-running workload containers.", - "examples": [ - 2 - ], - "minimum": 1, - "type": "integer" - }, - "resources": { - "additionalProperties": false, - "description": "Container memory and CPU limits.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "cpus": { - "description": "Container CPU limit expressed as a positive decimal count. Expects a number of CPUs such as 0.5 or 2.", - "examples": [ - "0.5" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?$", - "type": "string" - }, - "memory": { - "description": "Container memory limit. Expects a size such as 512MB or 1.5GB.", - "examples": [ - "512MB" - ], - "pattern": "^[0-9]+(\\.[0-9]+)?(B|KB|MB|GB|TB)$", - "type": "string" - } - }, - "type": "object" - }, - "role": { - "description": "Lifecycle role: application, worker, daemon, or job.", - "enum": [ - "application", - "worker", - "daemon", - "job" - ], - "examples": [ - "application" - ], - "type": "string" - }, - "routes": { - "description": "Ingress routes exposed by this workload.", - "items": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "hostname": { - "const": "*" - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - }, - "tls": { - "enum": [ - "none", - "passthrough" - ] - } - }, - "required": [ - "protocol", - "tls" - ] - } - }, - { - "if": { - "properties": { - "hostname": { - "pattern": "^\\*\\." - } - }, - "required": [ - "hostname" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "http" - } - } - } - }, - { - "if": { - "properties": { - "tls": { - "const": "passthrough" - } - }, - "required": [ - "tls" - ] - }, - "then": { - "properties": { - "protocol": { - "const": "tcp" - } - }, - "required": [ - "protocol" - ] - } - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "entrypoint": { - "default": "websecure", - "description": "Named proxy listener used for the route.", - "type": "string" - }, - "hostname": { - "anyOf": [ - { - "maxLength": 253, - "pattern": "^(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$" - }, - { - "const": "*" - } - ], - "description": "Hostname matched by the proxy. Accepts an exact hostname or a wildcard in the complete left-most label, such as *.example.com; a wildcard matches exactly one label and not the suffix itself. The bare * value is reserved for plaintext or TLS-passthrough TCP catch-all routes.", - "examples": [ - "shop.example.com" - ], - "type": "string" - }, - "middlewares": { - "description": "Ordered provider-qualified middleware references applied to this route.", - "items": { - "description": "Expects a provider-qualified name such as secure-headers@file.", - "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*@[a-z][a-z0-9-]*$", - "type": "string" - }, - "type": "array" - }, - "path": { - "default": "/", - "description": "URL path prefix matched by an HTTP route. Expects a path beginning with /.", - "pattern": "^/[^\\x00-\\x1f'\"$` \\\\]*$", - "type": "string" - }, - "port": { - "description": "Container port receiving routed traffic.", - "examples": [ - 3000 - ], - "maximum": 65535, - "minimum": 1, - "type": "integer" - }, - "protocol": { - "default": "http", - "description": "Routing protocol: http or tcp.", - "enum": [ - "http", - "tcp" - ], - "type": "string" - }, - "scheme": { - "default": "http", - "description": "Backend connection scheme for HTTP routes: http, https, or h2c.", - "enum": [ - "http", - "https", - "h2c" - ], - "type": "string" - }, - "tls": { - "default": "terminate", - "description": "TLS handling: terminate, passthrough, or none.", - "enum": [ - "terminate", - "passthrough", - "none" - ], - "type": "string" - } - }, - "required": [ - "hostname" - ], - "type": "object" - }, - "type": "array" - }, - "schedule": { - "additionalProperties": false, - "description": "Host-resident recurring schedule and run policy for a job, independent of its deployment phase and operator-run policy.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "catch_up": { - "default": true, - "description": "Run once after the host returns if an elapsed schedule was missed while it was offline.", - "type": "boolean" - }, - "cron": { - "description": "Five-field cron schedule translated to a host timer. Expects five cron fields.", - "examples": [ - "0 2 * * *" - ], - "pattern": "^[-0-9*/,A-Za-z ]+$", - "type": "string" - }, - "deploy_lock": { - "default": "exclusive", - "description": "Deployment coordination policy: exclusive blocks application operations for the full run; pinned leases the immutable starting release and permits only deployments without data-changing jobs or untyped hooks.", - "enum": [ - "exclusive", - "pinned" - ], - "examples": [ - "pinned" - ], - "type": "string" - }, - "notify": { - "default": [ - "failure", - "timeout" - ], - "description": "Run outcomes that send the configured notifications: success, failure, timeout, skipped.", - "items": { - "enum": [ - "success", - "failure", - "timeout", - "skipped" - ], - "type": "string" - }, - "type": "array" - }, - "retry": { - "additionalProperties": false, - "description": "Bounded retry inside one timer firing. Attempts run under the same locks and the same timeout; a timeout ends the run.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "attempts": { - "default": 1, - "description": "Total attempts including the first, 1 to 10.", - "examples": [ - 3 - ], - "maximum": 10, - "minimum": 1, - "type": "integer" - }, - "backoff": { - "default": "30s", - "description": "Sleep before the second attempt; it doubles after each failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "1m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "max_backoff": { - "default": "10m", - "description": "Upper bound for the doubling sleep. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - } - }, - "type": "object" - }, - "shutdown_grace": { - "default": "30s", - "description": "Time allowed for graceful container shutdown after the run deadline before Onebox forces removal. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "45s" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "timeout": { - "default": "1h", - "description": "Maximum wall time for one scheduled run before systemd terminates it and records failure. Expects a duration such as 30s, 5m, 1h30m or 14d.", - "examples": [ - "30m" - ], - "pattern": "^(([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+|[0-9]+d)$", - "type": "string" - }, - "timezone": { - "default": "UTC", - "description": "IANA timezone used to interpret the cron schedule. Expects an IANA zone name such as UTC or Europe/Berlin.", - "examples": [ - "Europe/Berlin" - ], - "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$", - "type": "string" - } - }, - "type": "object" - }, - "stdin_open": { - "description": "Keep standard input open for the container.", - "type": "boolean" - }, - "strategy": { - "description": "Replacement strategy for a changed or uncertain workload. An unchanged healthy workload is retained automatically. Defaults to rolling only for an application workload with health; all other workloads default to recreate.", - "enum": [ - "rolling", - "recreate" - ], - "type": "string" - }, - "tty": { - "description": "Allocate a pseudo-TTY for the container.", - "type": "boolean" - }, - "user": { - "description": "User or UID used to run the container process.", - "type": "string" - }, - "volumes": { - "description": "Managed named volumes or bind mounts. Relative bind sources are read-only release content; absolute sources are external host state.", - "items": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "source": { - "pattern": "^[^/]" - } - }, - "required": [ - "source" - ] - }, - "then": { - "properties": { - "mode": { - "const": "ro" - } - }, - "required": [ - "mode" - ] - } - } - ], - "anyOf": [ - { - "required": [ - "name", - "path" - ] - }, - { - "required": [ - "source", - "path" - ] - } - ], - "patternProperties": { - "^x-": {} - }, - "properties": { - "mode": { - "default": "rw", - "description": "Mount access mode: rw or ro. A relative bind source requires ro.", - "enum": [ - "rw", - "ro" - ], - "type": "string" - }, - "name": { - "description": "Stable logical name of a Onebox-managed volume. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters.", - "examples": [ - "data" - ], - "pattern": "^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$", - "type": "string" - }, - "path": { - "description": "Absolute container path where the volume or bind mount is attached. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/var/lib/app" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - }, - "source": { - "description": "Bind mount source. An absolute path is external host state that outlives releases. A dot-prefixed repository path is read-only release content, kept for as long as a container still mounts it. Expects an absolute host path or a dot-prefixed path inside the repository, with no colon, control character or shell metacharacter.", - "examples": [ - "./config" - ], - "not": { - "pattern": "(^|/)\\.\\.(/|$)" - }, - "pattern": "^(/[^\\x00-\\x1f'\"$`\\\\:]*|\\.(?:/[^\\x00-\\x1f'\"$`\\\\:]*)?)$", - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "working_dir": { - "description": "Absolute working directory for the container process. Expects an absolute path with no control character or shell metacharacter.", - "examples": [ - "/app" - ], - "pattern": "^/[^\\x00-\\x1f'\"$`\\\\]*$", - "type": "string" - } - }, - "type": "object" - }, - "description": "Application containers, workers, daemons, and jobs managed as releases.", - "type": "object" - } - }, - "required": [ - "api_version", - "environments" - ], - "title": "Onebox project (onebox.run/v2)", - "type": "object" -} diff --git a/site/src/components/landing/Derivation.astro b/site/src/components/landing/Derivation.astro index 47eac2df..573c53b1 100644 --- a/site/src/components/landing/Derivation.astro +++ b/site/src/components/landing/Derivation.astro @@ -219,7 +219,7 @@ const render = () => { const lines: string[] = [ - `api_version: onebox.run/v2`, + `api_version: onebox.run/v1`, `app: shop`, `environments:`, ` production:`, diff --git a/site/src/content/docs/guides/environment-variables.mdx b/site/src/content/docs/guides/environment-variables.mdx index 30eac528..b0ed70ab 100644 --- a/site/src/content/docs/guides/environment-variables.mdx +++ b/site/src/content/docs/guides/environment-variables.mdx @@ -112,7 +112,7 @@ no aliases, no fallback reads, and no deprecation window: Update your hook scripts, CI and release settings, and any job writing to the result file. The executable is still `ob`, the project file is still `ob.yml`, -and the API version is still `onebox.run/v2` — only the environment namespace +and the API version is still `onebox.run/v1` — only the environment namespace changed. A stray old name does not fall back: it is simply never read, so a hook reading diff --git a/site/src/content/docs/reference/cli.mdx b/site/src/content/docs/reference/cli.mdx index d0395348..dbedff09 100644 --- a/site/src/content/docs/reference/cli.mdx +++ b/site/src/content/docs/reference/cli.mdx @@ -953,7 +953,7 @@ Global Flags: ## ob preview ``` -Load an onebox.run/v2 project, resolve the environment's overrides, and print +Load an onebox.run/v1 project, resolve the environment's overrides, and print the Compose runtime Onebox would generate, with its content digest. Nothing is contacted and nothing is written. Environment values are redacted: @@ -1190,12 +1190,12 @@ Global Flags: ## ob schema ``` -Write the JSON Schema for the `onebox.run/v2` project file. +Write the JSON Schema for the `onebox.run/v1` project file. Reference it from the first line of a project so an editor can offer completion, hover documentation and inline errors: - # yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json + # yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v1.schema.json Or keep a copy in the repository with --out, which is what an editor needs when the machine is offline. diff --git a/site/src/content/docs/reference/fields/top-level.mdx b/site/src/content/docs/reference/fields/top-level.mdx index ae1fef88..a18c0887 100644 --- a/site/src/content/docs/reference/fields/top-level.mdx +++ b/site/src/content/docs/reference/fields/top-level.mdx @@ -24,7 +24,7 @@ cannot drift from what `ob validate` accepts. | Field | Type | Default | What it does | | --- | --- | --- | --- | -| `api_version` `*` | string | — | Project contract version. Must be onebox.run/v2. | +| `api_version` `*` | string | — | Project contract version. Must be onebox.run/v1. | | `app` | string | — | Stable application name used in generated container, volume, network, and host paths. The application's name. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters, and may not begin "ob-" or be a name the host layout reserves. | | `base_path` | string | `/var/lib/ob` | Absolute host directory beneath which Onebox stores application state and releases. Expects an absolute path with no control character or shell metacharacter. | | `build` | object | — | Build metadata for development. Production requires a resolved image supplied with --image. Also accepts a build context path. | diff --git a/site/src/content/docs/reference/policies.mdx b/site/src/content/docs/reference/policies.mdx index a4399b95..cad94337 100644 --- a/site/src/content/docs/reference/policies.mdx +++ b/site/src/content/docs/reference/policies.mdx @@ -1,13 +1,13 @@ --- title: Policies description: Versioning, runner compatibility, plan and approval schemas, and the guarantees that hold across releases. -summary: CalVer release identity, minimum runner and plan-schema policy, the executable plan and approval contracts, and the v2 evolution guarantees. +summary: CalVer release identity, minimum runner and plan-schema policy, the executable plan and approval contracts, and contract evolution guarantees. sidebar: order: 400 read_when: - "Pinning a minimum runner version for an environment" - "Understanding why a checkout build was refused" - - "Checking what the v2 contract guarantees across upgrades" + - "Checking what the project contract guarantees across upgrades" --- ## Release identity diff --git a/site/src/content/docs/reference/project-file.mdx b/site/src/content/docs/reference/project-file.mdx index e56ac727..28278d81 100644 --- a/site/src/content/docs/reference/project-file.mdx +++ b/site/src/content/docs/reference/project-file.mdx @@ -1,7 +1,7 @@ --- title: Project file description: The shape of ob.yml — required keys, shorthand forms, and where each block lives. -summary: Overview of the onebox.run/v2 project file — the block map, every scalar shorthand, and the two environment-value precedence rules. +summary: Overview of the onebox.run/v1 project file — the block map, every scalar shorthand, and the two environment-value precedence rules. sidebar: order: 1 read_when: @@ -10,7 +10,7 @@ read_when: - "Working out which block a field belongs to" --- -`onebox.run/v2` is the contract for one application on one host. It goes in +`onebox.run/v1` is the contract for one application on one host. It goes in `ob.yml` at the root of your repository. `ob.yaml` is accepted automatically when `ob.yml` is absent, and `-c` accepts either spelling or any explicit path. @@ -20,44 +20,43 @@ from a `workloads` block or the top-level shorthand. Start with the schema reference so your editor can help while you type: ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json -api_version: onebox.run/v2 +# yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v1.schema.json +api_version: onebox.run/v1 ``` `ob schema --out onebox.schema.json` writes a local copy, and `ob init` puts the published reference on the first line of a scaffolded project. -## Migrating from v1 +## Migrating route declarations -Version 2 makes every ingress rule explicit and uses the conventional -`hostname` name for both exact hosts and wildcard hosts. Change -`api_version: onebox.run/v1` to `api_version: onebox.run/v2`, then migrate route -fields as follows: +Every ingress rule is explicit and uses `hostname` for both exact hosts and +wildcard hosts. The contract identity remains `onebox.run/v1`; update retired +route fields as follows: ```yaml -# v1 scalar route shorthand +# retired scalar route shorthand domain: shop.example.com port: 3000 -# v2 +# current route routes: - hostname: shop.example.com port: 3000 ``` ```yaml -# v1 wildcard route +# retired wildcard route routes: - wildcard_suffix: preview.example.com port: 3000 -# v2: a complete leftmost wildcard label, matching one label only +# current: a complete leftmost wildcard label, matching one label only routes: - hostname: "*.preview.example.com" port: 3000 ``` -The v2 loader rejects v1 project files instead of silently changing their +The loader rejects retired route fields instead of silently changing their meaning. The workload-level `port` field remains available as the default container port for HTTP health checks; it no longer creates an ingress route. @@ -108,8 +107,8 @@ These are contract, not convenience that changes within a version. A single-workload project may write the workload's own fields at the top level instead of a `workloads` block: `build`, `image`, `compose`, `port`, `health`, -`routes`. Ingress is always declared through `routes`; v2 removes the v1 scalar -`domain`/`port` route shorthand. +`routes`. Ingress is always declared through `routes`; the scalar `domain`/`port` +route shorthand is not accepted. Mixing the two is `shorthand_and_workloads` — it would be ambiguous which workload the top-level fields describe. Shorthand also needs `app` to attach the @@ -341,7 +340,7 @@ generates the runtime from it. Individual services can still be adopted with ## Evolution -`api_version: onebox.run/v2` is stable. Within it: +`api_version: onebox.run/v1` is stable. Within it: - A field is **added**, never repurposed. - A scalar form once accepted is accepted **permanently**. diff --git a/site/src/content/docs/start/reading-it-back.mdx b/site/src/content/docs/start/reading-it-back.mdx index d85ed4a2..94e81ea5 100644 --- a/site/src/content/docs/start/reading-it-back.mdx +++ b/site/src/content/docs/start/reading-it-back.mdx @@ -40,8 +40,8 @@ ob schema --out onebox.schema.json Or reference the published copy from the first line of the project: ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v2.schema.json -api_version: onebox.run/v2 +# yaml-language-server: $schema=https://raw.githubusercontent.com/labstack/onebox/main/docs/onebox.run-v1.schema.json +api_version: onebox.run/v1 ``` The schema is generated from the same declarations the loader enforces and is diff --git a/site/src/content/docs/status/capabilities.mdx b/site/src/content/docs/status/capabilities.mdx index 1fe19868..53905170 100644 --- a/site/src/content/docs/status/capabilities.mdx +++ b/site/src/content/docs/status/capabilities.mdx @@ -23,7 +23,7 @@ This page is the reconciliation. Three states: ## Shipped -- A stable, explicit `onebox.run/v2` project schema. Future v2 evolution is +- A stable, explicit `onebox.run/v1` project schema. Future contract evolution is additive. - Compose generation and validation, SSH transport with known-host checking, state-bound plans, image pinning, rendered diffs. diff --git a/site/src/pages/llms.txt.ts b/site/src/pages/llms.txt.ts index f5213b04..844b1718 100644 --- a/site/src/pages/llms.txt.ts +++ b/site/src/pages/llms.txt.ts @@ -58,7 +58,7 @@ export const GET: APIRoute = async ({ site }) => { "", `- [Markdown page export](${origin}/start/first-deploy.md): Append \`.md\` to any docs page URL for clean Markdown.`, `- [Full documentation text](${origin}/llms-full.txt): Every page concatenated, for one-shot ingestion.`, - `- [Project file JSON Schema](${origin}/onebox.run-v2.schema.json): The machine contract the loader enforces.`, + `- [Project file JSON Schema](${origin}/onebox.run-v1.schema.json): The machine contract the loader enforces.`, `- [Sitemap](${origin}/sitemap-index.xml): Crawler URL index.`, "", "## Operating Onebox from an agent",