Connections
A connection is a record in CORE’s console registry. It says which external system a tenant’s data lives in, where it is, and which credential bundle unlocks it. The registry is the store/connections package, opened by the console (operators/core/internal/console/connections.go).
store/connections package, but each holds separate records. Nothing syncs, mirrors or reconciles CORE’s registry with an app’s.Create a connection in the console
DataEx › Connections (?tab=connections) is the only place to create, edit, test and rotate connections. It is the shared org-runink/ui datasources wizard (ConnectionsService), mounted by datasources_ui.go.
- Pick a source card. Cards map to types in the connector catalog. A card whose type the catalog lacks is shown as unavailable.
- Fill the settings the type reads. Enter credentials in their own step. Credentials are write-only: no read ever returns them.
- Choose the runner that will reach the source. Runink managed is stored as
""(the console process itself). A self-hosted runner is stored by name. See Data-access runners. - Save, then Test. Test is Resolve’s Test, run through the shared estate server on the connection’s bound runner. It is the one dialler, and there is no fallback runner.
Every change, Test and Verify goes through the handler’s own allowlist: CORE_CONNECTION_ADMINS for connection changes, and CORE_ATLAS_ADMINS for Test and Verify, because those dial the source. Every attempt is written to the audit chain. See Trust & access.
Resolve’s Add credentials and the onboarding wizard’s Add source open this same wizard.
The JSON registry API
These routes stay available for one release as deprecated aliases. core connection register still posts to them.
| Route | Method | Auth |
|---|---|---|
/api/connection-types | GET | session. The connector catalog: the settings and credential keys each type reads |
/api/connections | GET | session. Lists records. Answers 503, with the full report in the body, when the registry is unreadable |
/api/connections | POST | admin-write, CORE_CONNECTION_ADMINS |
/api/connections/{id} | GET | session. 404 no connection with id <id> when absent |
/api/connections/{id} | PUT, DELETE | admin-write, CORE_CONNECTION_ADMINS |
The write body (connectionWrite) is id, name, type, environment, runner, config, properties, credentialRef and credentials. On an update, omitting credentials leaves the stored bundle untouched. The body is capped at 256 KiB. A nested id such as a/b is refused rather than truncated, because the id names a file in the credential store.
The type is canonicalised on write, so Service Now, service-now and servicenow all store as one type. An unrecognised type is stored as-is, because the catalog is a known-types table, not a closed set.
Register connections from a manifest
core connection register <manifest> applies a committed manifest through the console’s own /api/connections:
export CORE_CONSOLE_URL=https://<your-console>
export CORE_CONSOLE_SESSION=<cookie> # or CORE_CONSOLE_USER + CORE_CONSOLE_PASSWORD
export MY_POSTGRES_HOST=… MY_POSTGRES_PASSWORD=… # from your secret store
core connection register my-estate.yaml --dry-run| Flag | Effect |
|---|---|
--dry-run | validate and report what would happen, writing nothing |
--adopt-drift | overwrite a console connection whose settings differ from the manifest (off by default: an edited connection is a decision) |
--rotate-credentials | replace the credential bundle of a connection that already exists (off by default, because saving REPLACES) |
The console URL comes from --console-url or CORE_CONSOLE_URL. There is no default. It must be https unless the host is loopback. The session (CORE_CONSOLE_SESSION, or CORE_CONSOLE_USER + CORE_CONSOLE_PASSWORD) is read from the environment only, never from core.yaml.
Manifest format
The repo’s connections/ directory ships only TEMPLATE.yaml and a README, on purpose. A host or an account locator is a fact about one installation’s estate, so a real manifest belongs in the repo that owns that estate.
apiVersion: runink.org/partner-connections/v1
partner: template # groups the file; documentation only
description: >-
…
connections:
- id: template-replace-this-id # plain identifier: names a credential file
name: TEMPLATE — replace this
type: postgres # must be in the TARGET console's catalog
environment: replace-me
settings:
host: ${TEMPLATE_POSTGRES_HOST}
port: 5432 # never a ${VAR}: numeric settings are refused as strings
database: ${TEMPLATE_POSTGRES_DATABASE}
username: ${TEMPLATE_POSTGRES_USERNAME}
ssl_mode: require
properties:
owner: ${TEMPLATE_POSTGRES_OWNER}
credentials:
- key: username
fromEnv: TEMPLATE_POSTGRES_USERNAME
- key: password
fromEnv: TEMPLATE_POSTGRES_PASSWORD${NAME}resolves insidesettingsandpropertiesonly, from the environment, at apply time. An unresolved reference blocks the connection, which is why applying the template unedited writes nothing.- A credential entry has exactly
keyandfromEnv. The YAML is decoded withKnownFields(true), so avalue:line is a parse error. - Declare an ownership property (
owner,owner_email,steward,data_stewardorteam). The datagov agent’sgovernance/owner-declaredcontrol checks exactly those keys. ssl_modevaluespreferandallowfall back to plaintext. Onlyrequire,verify-caandverify-fullcount as transport encryption.
The four no-secrets layers
The CLI reproduces these client-side (internal/connmanifest), because the server’s screening is narrower than it looks:
- Structural. Unknown fields are parse errors.
- Key screening. Settings and properties keys are screened with
store/connections’ secret-key rule (IsSecretKey). A settings key must also be a setting that the console’s catalog says the connector reads. - Value screening. Values are screened for embedded authentication (a DSN with
password=, a URL with userinfo), before and after${VAR}expansion. - Names, not values. The bundle is assembled from environment variable names at apply time. No error message echoes a value.
The server gap is real. Connection.Validate screens only Properties and Config.Extra, and the console decodes config with a plain json.Decoder. So host: "postgres://u:pw@h/db" would be accepted into the non-secret core-console-connections ConfigMap, and an unknown settings key is dropped without a word. Every write is read back and compared.
The report
| State | Meaning |
|---|---|
CREATED | nothing was there. The record is written, with its bundle if one was declared |
UNCHANGED | the same connection exists. It is not rewritten |
DRIFTED | a different connection exists. The differing fields are printed, nothing is written, and the exit code is 1. --adopt-drift overwrites |
ADOPTED | a drifted connection was overwritten because --adopt-drift was given |
BLOCKED | a declared credential or ${VAR} is unset. Nothing is written, and the missing variable names are printed |
Drift is decided before rotation. The console’s PUT writes the record and the bundle in one call, so rotating a drifted connection would adopt the drift.
Two columns are deliberately not answers:
- NOT CHECKED: registration establishes nothing about reachability, and there is no
--verifyand must not be one. The CLI never dials. CORE’s only connector runtime is the console’s Resolve, on an admin’s action, so use Resolve’s Test. - NOT REPORTED: whether a bundle is present. Answering that writes a
GET_CREDENTIALSaudit record per connection.
Connectors CORE publishes (not registry records)
The platform’s own connectors are configured under POST /api/providers (connectors[]) and published to connectorsync. They are not registry records, so Resolve cannot test them. GET /api/connectors/status never dials: it returns the last cached probe, and a connector that was never probed is NOT MEASURED. Only POST /api/connectors/status/refresh dials, and it is admin-write under CORE_CONNECTION_ADMINS. The types with a probe are listed in connectors.go: databricks, snowflake, mqtt, rfid, sensorgateway, gps, and the REST systems oms, tms, wms, yms, ims and whs. A type with no probe is reported measured:false. POST /api/providers merges by section: an absent or null key keeps what is stored, and "connectors": [] clears it.