Skip to content

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).

CORE’s records are CORE’s own. CORE and the apps share the 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.

  1. Pick a source card. Cards map to types in the connector catalog. A card whose type the catalog lacks is shown as unavailable.
  2. Fill the settings the type reads. Enter credentials in their own step. Credentials are write-only: no read ever returns them.
  3. 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.
  4. 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.

RouteMethodAuth
/api/connection-typesGETsession. The connector catalog: the settings and credential keys each type reads
/api/connectionsGETsession. Lists records. Answers 503, with the full report in the body, when the registry is unreadable
/api/connectionsPOSTadmin-write, CORE_CONNECTION_ADMINS
/api/connections/{id}GETsession. 404 no connection with id <id> when absent
/api/connections/{id}PUT, DELETEadmin-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
FlagEffect
--dry-runvalidate and report what would happen, writing nothing
--adopt-driftoverwrite a console connection whose settings differ from the manifest (off by default: an edited connection is a decision)
--rotate-credentialsreplace 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 inside settings and properties only, 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 key and fromEnv. The YAML is decoded with KnownFields(true), so a value: line is a parse error.
  • Declare an ownership property (owner, owner_email, steward, data_steward or team). The datagov agent’s governance/owner-declared control checks exactly those keys.
  • ssl_mode values prefer and allow fall back to plaintext. Only require, verify-ca and verify-full count 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:

  1. Structural. Unknown fields are parse errors.
  2. 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.
  3. Value screening. Values are screened for embedded authentication (a DSN with password=, a URL with userinfo), before and after ${VAR} expansion.
  4. 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

StateMeaning
CREATEDnothing was there. The record is written, with its bundle if one was declared
UNCHANGEDthe same connection exists. It is not rewritten
DRIFTEDa different connection exists. The differing fields are printed, nothing is written, and the exit code is 1. --adopt-drift overwrites
ADOPTEDa drifted connection was overwritten because --adopt-drift was given
BLOCKEDa 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 --verify and 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_CREDENTIALS audit 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.