Skip to content
docuconf
docuconf on GitHub

Draft, not final

v1beta1 does not exist yet. These pages describe the changes proposed for it in docuconf-go pull requests #27, #28, #29 and #30, which are not merged. Anything here can still change. Keep contracts on v1alpha1 until the format freeze lands. See the versioning policy and the draft SPEC.md.

Inputs

An application reads its configuration from two places: environment variables and files. A contract describes both. For each input it records the type, the constraints, whether it is required or secret, and — because libraries disagree — exactly how the app parses it.

The platform supplies each input from a source: a literal, a Kubernetes reference, an injector, a mounted file or a config-file overlay. Whatever the source, the value the app finally sees is checked twice: by the platform before deploy where it can be, and by the SDK at boot always.

Environment variables

vars is a map keyed by the variable name, which must match ^[A-Z][A-Z0-9_]*$. SDKs map idiomatic field names (databaseUrl, DatabaseUrl) to this form.

Variable types

Every SDK must parse each type identically, so a type exists only once the spec defines it. In v1beta1 the set can grow but never shrink: a new type is an addition, and a tool that does not know one reports it instead of guessing. v1beta1 adds keySet.

Variable types
TypeMeaningConstraint fieldsPlatform valueWire form
stringFree text.minLength, maxLength (in characters), pattern (RE2, matches anywhere unless anchored)stringas is
int64-bit signed integer, base 10 only: 007 is 7, and 0x10, 1_000 or 1e3 are invalid_type. SDKs export narrower host ranges as min/max.min, maxintbase-10, no leading + or zeros: 8080
floatFinite decimal number with a digit on each side of the point, parsed independently of locale. inf, NaN, .5 and 0,5 are invalid_type.min, maxnumbershortest round-trip decimal: 0.5
booltrue or false, in any case (TRUE, False). Nothing else: not 1, yes or on.—booltrue / false
durationA length of time, written in Go syntax on the platform side.min, max (durations), encodingGo-syntax duration: 1m30sper encoding: go, iso8601, seconds or timespan
urlAn absolute URL with a scheme.schemes, maxLength (the URL as it is)string with scheme://as is
enumOne of a fixed set of strings.values (non-empty)one of valuesas is
listA list of strings or integers.items (string | int), encoding, separator, minItems, maxItems, itemMin and itemMax (int items), itemMinLength and itemMaxLength (string items, after splitting)listper encoding: csv, json or indexed
keySetSecret keys that are all valid at once, so one can be rotated without an outage: webhook signatures, inbound API keys. Always secret.encoding, separator (csv), minKeys (default 1), maxKeys (default 2), keyMinLength, keyMaxLength (in characters)a secret reference onlyas a list of strings: old,new in csv
jsonA structured value, checked against a JSON Schema generated from the app’s own type.schema (JSON Schema), maxLength (the wire string)any JSON valuecompact JSON
  • pattern is RE2 and matches anywhere in the value; anchor it with ^…$ to match the whole value. RE2's \d, \w, \s and \b are ASCII-only.
  • json variables and config files carry a JSON Schema generated from the app's own type, so the platform checks structured values against the type the app deserializes into.
  • A keySet is a set of secret keys that are all valid at once, for the side that verifies: webhook signatures, inbound API keys, JWT HMAC verification. It is always secret, so it has no default and no examples. It travels in a list's wire encodings, and its keys are never trimmed. The number of keys outside minKeys..maxKeys is too_few_items or too_many_items; a key outside keyMinLength..keyMaxLength, and an empty key whatever the bounds (a stray separator), is out_of_range. See Secret rotation.
  • An empty string is a value for string. For every other type it means unset.

Length limits

  • Lengths count characters, meaning Unicode code points, never bytes or UTF-16 code units: 日本 is 2 characters, and ZÜ01 fits an itemMaxLength of 4. CUE's strings.MaxRunes and JSON Schema's maxLength count the same way, so the platform and every SDK agree.
  • minLength and maxLength bound a string, and maxLength also a url (the URL as it is) or a json value.
  • maxLength on a json value bounds its wire string: before deploy, the compact JSON the renderer writes; at boot, the raw value the app receives, whitespace included, before it is parsed. The Helm values schema cannot express it, so docuconf vet and the SDK check it.
  • itemMinLength and itemMaxLength bound each item of a string list, after the list is split, so a csv separator never counts. They are not allowed on an int list, as itemMin and itemMax are not allowed on a string list.
  • keyMinLength and keyMaxLength bound each key of a keySet the same way.
  • A value outside its limits is out_of_range at boot. A too-long secret is reported by its length, never its value.
  • An app that stores values in fixed-width byte fields, such as a COBOL PIC X(n), should declare a limit that leaves room for multi-byte characters, or restrict values to ASCII with a pattern such as ^[ -~]*$.

Fields on every variable

Variable fields
FieldRule
typeOne of the variable types. Beta may add a type; a tool that does not know one reports it instead of guessing.
descriptionRequired. Plain text, at least 5 characters: what the input is, in one phrase. Rendered into generated docs.
detailsOptional. CommonMark: why the input exists and when to change it. Not blank, at most 4000 characters. Used only in generated docs, never at runtime.
requiredDefault false. A required variable cannot have a default, and cannot be deprecated. Whether an input is optional is the app’s choice.
defaultMust satisfy the variable’s own constraints (checked at declaration time).
secretThe value must come from a secretKeyRef. No default, no examples, never printed.
groupFree-form grouping for docs.
examplesExample values, as strings, for docs.
deprecated{message, replacedBy?}, for staged removal: the platform should stop setting the input. message is not blank and at most 500 characters. A deprecated input that is still set is a warning, never an error, and SDKs warn at boot.
configKeyThe app’s own config key (Orders:Timeout, orders.timeout), for docs.

Deprecating an input

An input that is going away is marked deprecated first, so the platform can stop setting it before it disappears. This works for variables and file inputs alike:

OLD_PORT: {
	type:        "int"
	description: "Port the service listened on before 2.0"
	deprecated: {message: "Use PORT instead", replacedBy: "PORT"}
}
  • message says what to use instead, or why the input is going away: not blank, and at most 500 characters. replacedBy names the input that replaces it.
  • A required input cannot be deprecated, since the platform could not stop setting it.
  • The platform sees a warning, never an error. #Validate lists every deprecated input the platform still sets, as a value, an overlay value or a file source, in deprecatedSet. docuconf vet prints one line per warning with its message, and exits 0 when there are only warnings.
  • The unknown-name check stays strict. Values that set an input the contract does not declare are always rejected; deprecation is how an input is removed in stages. docuconf diff reports a deprecation as notable, and a removal as breaking for the platform, saying when the input was already deprecated.
  • SDKs should log a warning at boot for each deprecated input that is set, naming the input and its message, never its value. The input still loads and is still checked.
  • The generated docs show the notice and link to the replacement, and tell agents not to add uses of deprecated inputs.

This is your own inputs' deprecation. How docuconf deprecates its own fields and APIs is on Versioning and deprecation.

Value sources

A value is usually a literal. Some only exist in the cluster or are supplied at runtime, so the platform may give a reference instead:

Value sources
SourceExampleAllowed forCheckedStatus
literalPORT: 9090every non-secret typebefore deployImplemented
configMapKeyRef{configMapKeyRef: {name: "limits", key: "rate"}}every non-secret type except listat bootImplemented
fieldRef (Downward API){fieldRef: {fieldPath: "metadata.namespace"}}stringalways validImplemented
resourceFieldRef{resourceFieldRef: {resource: "limits.memory"}}intalways validImplemented
secretKeyRef{secretKeyRef: {name: "db", key: "url"}}secret variables, including every keySet (one of the two secret sources)at bootImplemented
injected, with a reference{injected: {provider: "bank-vaults", ref: "vault:secret/data/db#url"}}every type, including secrets, but not a list or key set in the indexed encoding; rendered verbatim as the env value for the injector to resolvereference shape before deploy; the value at bootImplemented
injected, without a reference{injected: {provider: "otel-operator"}}every type, including secrets; not rendered: a webhook, init process or operator sets itat bootImplemented

A secret variable is never a literal. It comes from a secretKeyRef or from an injector, and its content is checked only at boot. No docuconf tool ever prints a secret value.

Cloud-native injection

Many platforms do not put configuration into the pod spec at all. A mutating webhook, an init process or a wrapper resolves secrets at startup and hands the app a ready environment, or writes files the app reads. docuconf supports this by design:

  1. The SDK validates the process as it actually starts. Configuration is read from the real process environment and filesystem at startup, after any injector has run. A value from Bank-Vaults or op run is checked exactly like one from the pod spec.
  2. The contract does not care who supplies a value. It describes what the app accepts. The platform's values document says who supplies it.
  3. The platform declares injected inputs as injected, naming the provider and, when the injector reads a reference from the env value, that reference. docuconf vet checks the reference's shape, not the secret, and renders it verbatim for the injector to resolve.
# Platform values for orders-api
DATABASE_URL:
  injected: {provider: bank-vaults, ref: "vault:secret/data/orders/db#url"}
OTEL_EXPORTER_OTLP_ENDPOINT:
  injected: {provider: otel-operator}        # set by the operator; not rendered
LOG_LEVEL: warn                               # an ordinary literal
Runtime injectors
InjectorWhat it doesHow the contract describes it
Bank-Vaults (vault-env webhook)Env values like vault:secret/data/db#url are resolved by vault-env, which then execs the app with real values.injected value source with ref; the SDK sees and validates the resolved value.
Vault Agent injectorWrites secrets as files under /vault/secrets, rendered from templates.injected file source, with the pod annotations that make the agent write the file at its declared path; the SDK checks the file at boot like any other.
External Secrets OperatorSyncs a cloud secret manager into a Kubernetes Secret.secretKeyRef or secret file source, as usual; nothing special.
Secrets Store CSI driverMounts secrets from Vault, AWS, Azure or GCP as files.csi file source.
1Password, Doppler, Infisical and similar wrappersA wrapper process (op run, doppler run) resolves references and execs the app.injected value source, with the wrapper’s reference format as ref.
Operators and webhooks (OpenTelemetry, service meshes)Add variables such as OTEL_EXPORTER_OTLP_ENDPOINT to the pod.injected value source without ref, so the platform knows who supplies it, with the pod annotation or label that switches the injector on.

SDKs validate injected values like any other, because they read the process environment at startup, after injection. If a secret still holds a reference (vault:, op://, ref+), the injector did not run, and the SDK fails with invalid_type, naming the variable and the reference scheme but never the value.

Enabling the injector

Most injectors are switched on by annotations or labels on the pod. An injected source, for a variable or a file, may carry them as podAnnotations and podLabels, maps of strings. The values document may also have its own podAnnotations and podLabels, beside the variables, for settings every injected input shares, such as the Vault role. The contract does not change: which injector runs is a fact about the cluster.

  • Placeholders let an annotation that names one input be written once, beside it: {input} is the variable's or file's name, and for a file {path} is its path, {dir} the directory it occupies and {file} its file name. Nothing else is expanded, so a Vault Agent template's {{ .Data }} stays as it is. A placeholder the source does not define ({path} on a variable, any placeholder in the shared maps) is an undefinedPlaceholder error.
  • Rendering. docuconf render merges the shared maps and every injected input into two maps, podAnnotations and podLabels, for the pod template's metadata: webhooks see pods, not Deployments. In Helm, the library chart's docuconf.podAnnotations and docuconf.podLabels helpers return the same maps. Keys must be Kubernetes qualified names and label values valid label values, checked after expansion.
  • Conflicts. The same key with the same value from several inputs is fine. The same key with different values is a conflictingPodAnnotation or conflictingPodLabel error naming the key and both sources, in docuconf vet, in the render and in the Helm helpers.
  • Not secret. Anyone who can read the pod can read its annotations, so they hold references and settings (a Vault path, a role, a template), never secret material.

The Vault Agent injector writing a secret config file db-creds, declared at /vault/secrets/db.json:

# values.yaml
podAnnotations:                      # shared: one agent per pod, one role
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/role: ledger
# files.yaml
db-creds:
  injected:
    provider: vault-agent
    podAnnotations:
      vault.hashicorp.com/agent-inject-secret-{input}: database/creds/ledger
      vault.hashicorp.com/agent-inject-template-{input}: '{{- with secret "database/creds/ledger" -}}{{ .Data | toJSON }}{{- end }}'
      vault.hashicorp.com/secret-volume-path-{input}: "{dir}"
      vault.hashicorp.com/agent-inject-file-{input}: "{file}"

renders the two shared annotations plus agent-inject-secret-db-creds, agent-inject-template-db-creds, secret-volume-path-db-creds: /vault/secrets and agent-inject-file-db-creds: db.json, so the agent writes the file exactly at the declared path. Values are strings: write "true", not true. See SPEC.md §4.5.2 for Bank-Vaults, the OpenTelemetry operator and the edge cases.

Files

files is a map keyed by input name, a DNS label because it names a volume.

File types

File input types
TypeContentConstraint fieldsSecret
configA structured config file in format json, yaml or toml.format, schema (JSON Schema generated from the type the app binds the file to)optional
tlsA key pair in the kubernetes.io/tls layout: tls.crt, tls.key, and ca.crt when requireCA is set.dnsNames, keyAlgorithms (RSA, ECDSA, Ed25519), minRemaining, requireCAalways
caBundleOne or more PEM CA certificates.minCertificates (default 1)optional
keystoreA PKCS#12 or JKS keystore.format, passwordVar (a declared secret variable)always
textA text file, such as a licence key.pattern (RE2), minLength, maxLengthoptional
binaryOpaque bytes, such as a GeoIP database.maxSize onlyoptional

Fields on every file input

File input fields
FieldRule
typeOne of the file types.
description, details, required, group, deprecatedAs for variables.
secretContent must come from a secret store. Forced true for tls and keystore.
pathWhere the app reads it: a directory for tls, a file otherwise. Absolute.
pathEnvA variable the platform sets to path (SSL_CERT_FILE). Not also declared in vars.
reloadrestart (default): a changed source rolls the pods. watch: the app reloads it.
maxSizeUpper bound in bytes.

Mount rules, enforced by the meta-schema: a file is mounted at its parent directory, and a TLS pair at its own directory; no two inputs share a directory; none is mounted over a reserved directory such as /, /etc, /etc/ssl/certs, /usr, /var or /app; files are projected with items, never subPath, so rotation reaches them; secret files are mode 0400, others 0444.

File sources

File sources
SourceForChecked before deployStatus
inlinenon-secret filesEverything: format, schema, pattern, size, certificate count. Rendered as an immutable, content-hashed ConfigMap. Config files may be given as structured data.Implemented
configMapnon-secret filesThat a key is given for single files.Implemented
secretanyKey presence; with resolved metadata, the Secret’s type (kubernetes.io/tls) and its keys.Implemented
certificate (cert-manager)tlsFrom the Certificate spec: covers dnsNames, uses an allowed key algorithm, renewBefore ≥ minRemaining.Implemented
csi (Secrets Store CSI driver)anyNothing; checked at boot.Implemented
image (image volume)non-secret filesNothing; for data above the 1 MiB ConfigMap limit.Implemented
injected (Vault Agent and similar)anyThat the injector is named, and its pod annotations and labels; no volume is rendered, the injector writes the file at path. Checked at boot.Implemented

Resolved fields — a Secret's type and keys, a Certificate's spec — are metadata the platform tooling reads from the cluster. They are never secret contents. When absent, those checks move to boot.

Rotation. reload: watch means the app rereads the file, so the platform does nothing. reload: restart makes the platform roll the pods when the source changes. Inline content is content-hashed, so it always rolls the pods. For secrets in environment variables and keys with an overlap, see Secret rotation.

Secret rotation

docuconf delivers secrets and checks them. It does not store, issue or rotate them: Vault, External Secrets, cert-manager and the Secrets Store CSI driver do that. What the contract decides is how a rotated value reaches the running app, and that depends on how the secret is delivered.

Delivered asHow a rotated value reaches the app
Env var from a Secret (secretKeyRef)Environment variables are read once, when the process starts. A rotated value reaches the app on the next restart or redeploy, which the platform owns.
File secret (a TLS key pair, a Vault Agent file, a CSI volume)reload: watch: the app rereads the file and nothing restarts. reload: restart: the platform rolls the pods when the source changes. Files are projected with items, never subPath, so updates reach the mounted file.
Dynamic secret with a lease (Vault database credentials)Deliver it as a file written by Vault Agent or the CSI driver, with reload: watch, so each renewal reaches the app. Or the app fetches it itself, and the contract declares the Vault address and role instead of the credential.
Injected env value (vault-env, op run)Resolved when the process starts, so a rotated value needs a rollout.
API key with an overlap, two keys valid at onceDeclare both keys; see below.

Two valid keys during a rotation

An API key or signing key can be rotated without downtime if, for a while, both the old and the new key work. Who needs the overlap depends on the app's side of the call.

The app verifies keys (it accepts webhooks or API calls). Declare a keySet:

WEBHOOK_KEYS: {
	type:         "keySet"
	secret:       true
	description:  "Keys that verify the signature on incoming payment webhooks"
	minKeys:      1   // the default
	maxKeys:      2   // the default
	keyMinLength: 32
	keyMaxLength: 256
}

The platform supplies it like any secret, as one secretKeyRef, and the Secret holds the keys joined by commas, old,new during the overlap. Because the value is a reference, the checks run at boot: an empty key (a trailing comma), a truncated one or a third key fails with out_of_range or too_many_items instead of locking callers out. SDKs expose the keys in order, and should offer a constant-time contains and a helper that tries every key with a check you supply, such as an HMAC comparison. To rotate:

  1. Add the new key (old,new) and roll out. The app now accepts both.
  2. Switch the sender (the caller, or the signer) to the new key.
  3. Remove the old key (new) and roll out.

The generated docs print these steps for every key set, so an app's details need not repeat them.

A host that cannot offer a key set may declare two variables instead, a required API_KEY and an optional API_KEY_PREVIOUS, with the same length limits, and accept either:

API_KEY: {
	type:        "string"
	description: "The key callers send: the current key"
	required:    true
	secret:      true
	minLength:   32
	maxLength:   256
}
API_KEY_PREVIOUS: {
	type:        "string"
	description: "The previous key, still accepted while callers switch to API_KEY"
	secret:      true
	minLength:   32
	maxLength:   256
}

The platform sets API_KEY_PREVIOUS (a secretKeyRef) only during a rotation: move the old key there and the new key to API_KEY, roll out, switch the callers, then drop API_KEY_PREVIOUS and roll out again.

v1alpha1 recommended a secret list of strings with minItems: 1, maxItems: 2 and item length limits. That still works, with the same wire format, but a keySet says what the list is for, rejects an empty key without a length limit, and gets its rotation steps in the docs. Changing a variable from such a list to a keySet changes its type, which docuconf diff reports as breaking for the contract; the Secret and the values need no change.

What the platform cannot check. A rotation is only safe if every rollout keeps one key in common with the one before it. The platform supplies a reference and never sees the keys, so neither #Validate nor docuconf vet can check this: following the steps in order is the operator's job.

The app sends a key (it calls another service). Declare one secret key. Rotate it by updating the Secret once the receiving service accepts the new key; the app picks it up on its next restart or redeploy.

Either way, docuconf's part is the contract, the check before deploy (the value must be a reference, never a literal) and the check at boot (count and length). Issuing the new key, storing it and deciding when to rotate stay with the secret manager.

Config-file overlays

Hosts such as .NET, Spring Boot and Rails layer configuration files under environment variables. A common cloud-native pattern keeps the app's baked-in files and adds one more file, mounted by the platform, between them and the environment:

var builder = WebApplication.CreateBuilder(args);

// 1. Baked into the image: defaults and per-environment values.
builder.Configuration.AddJsonFile("appsettings.json", optional: false);

// 2. Mounted by the platform from a ConfigMap, reloaded on change.
builder.Configuration.AddJsonFile("/app/config/appsettings.Production.json", optional: true, reloadOnChange: true);

// 3. Environment variables (including injected secrets) win.
builder.Configuration.AddEnvironmentVariables();

A contract describes the overlay, and the platform renders it:

overlays: platform: {
	format: "json"
	path:   "/app/config/appsettings.Production.json"
	reload: "watch"   // reloadOnChange: true
}
  • Precedence is fixed and the same on every host: baked-in base file, then profile file, then the platform overlay, then environment variables.
  • The platform routes variables to the overlay in its values document. docuconf render writes each value at the variable's configKey (Orders:CheckoutTimeout becomes {"Orders": {"CheckoutTimeout": …}}) in native types — numbers, booleans, arrays — rather than env-var strings, and ships the file as a ConfigMap mounted at path.
  • Validation is identical: overlay values are typed values checked by #Validate, the same as env values. Secrets never go in an overlay, because overlays are ConfigMaps.
  • Overlays add, never replace. The overlay sits at its own path, so the app's baked-in appsettings.Production.json and its values still apply. Mounting over the baked-in file would silently discard them, and the meta-schema rejects an overlay path that is also a baked-in config file.
  • reload: watch maps to the host's reload-on-change, so the app sees new values without a restart (in .NET, through IOptionsMonitor<T>). restart rolls the pods instead.
  • The SDK reads the overlay exactly as #Render writes it. It loads the file as optional, from under DOCUCONF_FILE_ROOT like a file input: a missing overlay is not an error, and one that does not parse, or does not hold an object at its top level, is file_malformed. Each variable's value is read at its configKey, case-sensitively, converted to the wire string it stands for (50.0 is 50, a null is unset), and then parsed like an env value.
Hosts that layer config files
HostBaked-in filesPlatform overlayReload
.NET (Microsoft.Extensions.Configuration)appsettings.json, appsettings.{Environment}.jsonAddJsonFile("/app/config/appsettings.Production.json", optional: true, reloadOnChange: true)IOptionsMonitor<T> sees changes; IOptions<T> needs a restart
Spring Bootapplication.yml, application-{profile}.ymlspring.config.additional-location=file:/app/config/restart, or Spring Cloud refresh
Rails (anyway_config)config/<name>.ymlan extra YAML file read before envrestart
Rust (figment)Config.toml with profilesToml::file("/app/config/Config.toml") merged before Envrestart
Kotlin (Hoplite)application.conf / .yamlan extra property source ahead of the defaultsrestart, or Hoplite reloadable config

docuconf render and the Helm library chart write the overlay's ConfigMap, volume and mount. The SDKs for hosts that layer files load it in the right place; see the SDK table.

Wire encodings

Environment values are strings. The platform always holds typed values — lists as lists, durations in Go syntax — and the renderer writes each one in the form the app's library parses. For most types every library agrees. Lists and durations differ, so the contract records the app's encoding:

List encodings
List encodingWire formNative to
csv (default)a,b joined by separatorcaarlos0/env, Spring Boot, anyway_config
json["a","b"]pydantic-settings
indexedNAME__0=a, NAME__1=bMicrosoft.Extensions.Configuration
Duration encodings
Duration encodingWire form for 90sNative to
go (default)1m30sGo time.ParseDuration
iso8601PT1M30Spydantic timedelta, ActiveSupport::Duration, java.time.Duration (Spring)
seconds90anything that takes a number
timespan00:01:30.NET TimeSpan.Parse

Renderers double every $ in a literal ($ becomes $$), because Kubernetes expands $(NAME) in env values. Platform authors never see encodings: they write "90s" and ["a", "b"] for every app. A keySet uses the same encodings and separator as a list of strings, with keys for items.

Exact parsing

There is one parsing rule per type, and it is exact: an SDK accepts exactly the strings below, whatever its host library accepts on its own. Where the host is more lenient (strconv.ParseBool takes 1 and t, float() takes inf and spaces, Integer("010") is octal), the SDK adds a pre-check that rejects the extra forms with invalid_type. The platform only ever renders the canonical forms; these rules decide what an app accepts from values set outside the platform, so that every SDK accepts the same ones.

TypeAcceptedRejected (invalid_type)
every typethe value as given: never trimmed, ASCII digits only" true", "8080\n", "5s "
booltrue or false in any case: TRUE, False1, 0, t, yes, on
int^[+-]?[0-9]+$, base 10: 007 is 70x10, 0o17, 1_000, 1e3, 1.0; outside 64 bits is out_of_range
floata digit on each side of the point, optional exponent: 0.5, 1e-3.5, 5., inf, NaN, 0x1p4, 0,5, 1e400
duration (go)Go's time.ParseDuration grammar: 1m30s, 1.5h, -5s, 05, 5S, 1d, 1m 30s
duration (iso8601)P[nD][T[nH][nM][nS]], upper case, , or . for a fractionyears, months, weeks, a sign
duration (seconds, timespan)^[0-9]+(\.[0-9]+)?$; [d.]hh:mm:ss[.f]a sign, an exponent
list and keySet itemssplit on every separator, without trimming: a, b is a and ban empty or space-padded int item
jsonone JSON value, with only JSON whitespace around ittrailing text

The conformance suite tests these rules under the strict-parsing tag. See SPEC.md §5 for the full grammar.

Profiles

Values in files the image ships with are part of what the app runs with, so the contract includes them. A value in an always-loaded base file (appsettings.json, application.yml) is a default. A value in a profile file (appsettings.Staging.json) goes in profiles.defaults, applied when the selector variable (ASPNETCORE_ENVIRONMENT, SPRING_PROFILES_ACTIVE, RAILS_ENV) selects that profile. A required variable is satisfied when the platform or the selected profile supplies it. A secret may never have a value in any file.