ODCS data contracts

Import, validate, export and sync data contracts in the Open Data Contract Standard.

The ODCS tool (nistor-tool-odcs, tool id odcs) lets Nistor speak the Open Data Contract Standard, version 3.1.0. It validates a contract against the standard's JSON Schema, imports a contract that a producer wrote, exports the live state of a node as a contract, and syncs a repository of contracts from CI. It talks to no outside system: a contract is a YAML document you hand to Nistor. The classifier calls the package Alpha (see the overview).

What needs an Enterprise module#

Validation, import and export work in a pure open-source install. What a contract is projected onto depends on which other parts are present:

Part of an import Needs
The contract stored verbatim and re-exported without loss Open source
A Data Trust contract and a monitored asset, with a schema dimension (checked against the node's schema) and a freshness dimension (from a latency SLA) Open source. See Data Trust.
A volume dimension (from a row-count rule) and a quality dimension Open source to declare them. They are measured from data-quality results, so they have no data without the Data quality module (Nistor Enterprise).
Columns and the description written to the catalog, with precedence over Castor and BigQuery The Catalog module
Quality rules proposed as Nistor checks (the migration provider) The Data quality module
Drift: declared columns against the live catalog columns The Catalog module. Without it, GET /contracts/{id}/drift reports every declared column as missing.

An export has the node's live columns (from its table schema), its freshness SLA and its stored contract. Descriptions from the catalog and attached checks appear in it only with those modules.

Write a contract#

A contract is a YAML document with apiVersion: v3.1.0, kind: DataContract, an id, a version and a status. This one describes a BigQuery table:

apiVersion: v3.1.0
kind: DataContract
id: acme-orders
name: acme_orders
version: 1.0.0
status: active
domain: sales
description:
  purpose: One row per order, for finance reporting.
team:
  - username: data-team@example.com
    role: owner
schema:
  - name: orders
    physicalName: orders
    physicalType: table
    properties:
      - name: order_id
        physicalType: STRING
        logicalType: string
        required: true
        primaryKey: true
      - name: amount
        physicalType: NUMERIC
        logicalType: number
        required: true
      - name: ordered_at
        physicalType: TIMESTAMP
        logicalType: date
        required: true
    quality:
      - type: library
        rule: rowCount
        dimension: completeness
        mustBeGreaterOrEqualTo: 1000
slaProperties:
  - property: latency
    value: 24
    unit: hours
    element: orders.ordered_at
customProperties:
  - property: nodeId
    value: BigqueryTable_acme-data-prod.sales.orders

Nistor reads the contract like this:

  • The node. The nodeId custom property (or node_id) binds the contract to a graph node. You can also pass the node in the import request, which wins. A contract with no node is stored but projected onto nothing. Only the first object of schema is projected; the others are kept in the stored document.
  • Columns. The properties of the first object. required: true makes a column non-nullable. classification and criticalDataElement become tags.
  • SLA. A latency property becomes the freshness target in hours. frequency and timeOfAvailability are recorded. retention is kept in the document and not enforced.
  • Dimensions. The contract above produces four on its Data Trust contract: schema (the three columns, extra columns allowed), freshness (24 hours), volume (at least 1000 rows, from the rowCount rule) and quality (a pass rate of 95 per cent, when the contract has any executable rule).
  • Relationships are parsed and kept in the stored document. They are not written as links.

Data Trust evaluates a contract every 30 minutes. A dimension that breaches opens a contract breach and an incident, and a recovery resolves them. A dimension with nothing to measure is left out of the compliance score. See Incidents.

Validate, import and export#

The API is under /api/v1/odcs. Reading needs datatrust:view, and importing and deleting need datatrust:edit.

# Validate: returns the errors and a summary
curl -X POST "$NISTOR_API_URL/api/v1/odcs/validate" \
  -H "Authorization: Bearer $NISTOR_API_TOKEN" -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile y orders.odcs.yaml '{yaml_content: $y}')"

# Import: a document that fails validation is refused with 422
curl -X POST "$NISTOR_API_URL/api/v1/odcs/import" \
  -H "Authorization: Bearer $NISTOR_API_TOKEN" -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile y orders.odcs.yaml '{yaml_content: $y}')"

# Export a node's live state as a contract
curl -H "Authorization: Bearer $NISTOR_API_TOKEN" \
  "$NISTOR_API_URL/api/v1/odcs/node/BigqueryTable_acme-data-prod.sales.orders/export?download=false"

An import of an unchanged document is a no-op, because the stored row is keyed on a hash of its content. A document is limited to ODCS_MAX_YAML_BYTES (2,000,000). The export merges the stored contract under the live state, so keys Nistor does not model (pricing, roles, retention, relationships) survive a round trip, and the live state wins where they overlap.

Other routes: GET /contracts, GET /contracts/{id}, GET /by-node/{node_id}, DELETE /contracts/{id} and GET /health.

Contracts as code#

Keep the contracts in a repository and push them from CI:

nistor odcs sync --env prod --contracts-dir contracts/ --source-repo acme/data-contracts --delete-orphaned

nistor odcs sync sends the files to POST /api/v1/odcs/contracts/sync. For each one it replaces {ENV} with the environment, validates the document, and imports it as a code-managed, locked contract. A file that fails is reported and the batch goes on.

Option Meaning
--env Required: dev, test or prod.
--files or --contracts-dir The files, or a directory searched for *.odcs.yaml. Always pass one: the default directory is a path of Nistor's own deployment repository.
--source-repo, --source-branch The repository path recorded on each contract, and the branch (detected from CI or Git when omitted).
--delete-orphaned Soft-delete code-managed contracts of this repository that are no longer in the set. It refuses to run when no file is found.
--dry-run Report what would happen.
--output-json Write the report to a file.
--continue-on-error Exit 0 on a partial failure.

Use {ENV} inside a contract where an identifier differs between environments, as in the node id of a project named acme-data-{ENV}.

The connection#

A connection of type odcs describes the repository that holds the contracts:

Field Required Meaning
repo_url yes The repository address: https://, http://, ssh:// or git://, or the git@host:path form. A credential in the address is refused.
branch yes The branch. Default main.

The secret is token, and nothing reads it. The test checks that the address and the branch are well formed and says so: "repository not contacted (CI push model)". Nothing else reads the connection; the sync route reads the settings below.

Switch the feature off#

The feature is the capability odcs. It is on unless an administrator turns it off, with PATCH /api/v1/capabilities/odcs and a body of {"enabled": false}. Turning it off removes /api/v1/odcs.

Settings#

Variable Default Meaning
ODCS_GIT_REPO_URL empty Only decides whether POST /sync and the health banner call the pull "configured". The pull itself is not implemented.
ODCS_GIT_BRANCH main The branch of that stub.
ODCS_CONTRACTS_GLOB **/*.odcs.yaml The file pattern of that stub.
ODCS_SYNC_SCHEDULE_CRON, ODCS_SYNC_SCHEDULE_TZ, ODCS_SYNC_SCHEDULE_DESCRIPTION 0 5 * * *, Europe/Paris, "Daily at 05:00 Europe/Paris" Shown in the shell as the sync schedule. They fire nothing.
ODCS_MAX_YAML_BYTES 2000000 The largest document accepted.

Edit this page on GitLab

↑ ↓ to moveEnter to openEsc to close