Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vale/styles/spelling-exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ REST
RIRs
rowcount
Rowcount
rumdl
schema_mapping
sdk
Slurp'it
Expand Down
42 changes: 33 additions & 9 deletions dev/knowledge/orchestration-prefect.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@

`infrahub_sync/orchestration/` is the direct Prefect integration: a flow that runs one plan
or one confirmed sync, and a serve entrypoint that exposes it as a locally served
deployment. It is the only package in the repository that imports `prefect`, it is installed
by the optional `prefect` extra, and nothing in the base package imports it — see
deployment. It is installed by the optional `prefect` extra, and nothing in the base
package imports it — see
[ADR 9](../adr/0009-optional-integrations-live-in-their-own-package.md).

Two packages under `infrahub_sync/` import `prefect`, and both are optional: this one, and
`infrahub_sync/service/` below, which the `service` extra installs. No other package under
`infrahub_sync/` imports it, so a base install loads neither. The vendored
`opsmill_prefect_extras/` imports Prefect too; it sits outside `infrahub_sync/` and the
`service` extra is what brings it into a working install.

The flow calls [the shared execution surface](execution-surface.md) in-process. It never
spawns the CLI.

Expand All @@ -36,11 +42,29 @@ Exactly those four parameters. None of them accepts a path, a CLI fragment, a cr
an environment override. Everything else the run needs comes from the serving process's own
environment.

The separate `infrahub-sync-service/run` deployment accepts exactly seven parameters:
`run_id`, `sync_name`, `stage`, `configuration_reference`, `branch`, `expected_checksum`,
and `confirm_writes`. It does not replace or extend the four-parameter direct Prefect
flow. Credentials, endpoints, adapter instances, product-cache locations, and saved-plan
cache locations stay in the service worker environment.
The separate `infrahub-sync-service/run` deployment serves a different flow, with eight
parameters:

```python
@flow(name="infrahub-sync-service")
def service_sync_run(
run_id: str,
stage: Literal["plan", "verify", "apply", "sync"],
config_id: str | None = None,
registry_version: int | None = None,
package_checksum: str | None = None,
branch: str | None = None,
expected_checksum: str | None = None,
confirm_writes: bool = False,
) -> dict[str, Any]: ...
```

The three registry parameters travel together: when all three are set, they name the
registered version the run is bound to; when all three are unset, the run uses the legacy
path; and a partial carrier is refused.
This flow does not replace or extend the four-parameter direct Prefect flow. Credentials,
endpoints, adapter instances, product-cache locations, and saved-plan cache locations stay
in the service worker environment.

When the service flow runs outside Prefect context in offline executor tests,
`get_run_logger()` raises `MissingContextError`. The flow catches only that exception and
Expand All @@ -62,7 +86,7 @@ SUMMARY_LINE_FORMAT = "run %s finished: status=%s changed=%s summary=create:%d,u
```

This line is how a remote caller reads a run's outcome, and its format is contractual —
never a Python dict repr. It carries five `RunResult` fields: `run_id` (the leading
never a Python dictionary `repr`. It carries five `RunResult` fields: `run_id` (the leading
substitution), `status`, `changed`, the three summary counts, and `artifact_path`.
`sync_name` and `operation` deliberately do not appear. Changing the format is a breaking
change for consumers.
Expand Down Expand Up @@ -127,7 +151,7 @@ The directory path is fixed at serve start; its *contents* are re-resolved on ev
configurations added, edited or removed take effect on the next run without re-serving.

The serve process must be started from the repository root for the shipped example to work:
its `config.yml` uses repo-root-relative paths resolved against the serving process's
its `config.yml` uses repository-root-relative paths resolved against the serving process's
working directory, and the cache root defaults to `Path.cwd()/.infrahub-sync-cache`. Started
elsewhere, the example degrades to a silently empty plan or an adapter import failure.

Expand Down
66 changes: 53 additions & 13 deletions docs/docs/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ This guide covers how to set up a development environment for `infrahub-sync` an

## Prerequisites

- Python 3.10–3.13 (3.12 recommended)
- Python 3.11–3.13 for the full development profile (3.12 recommended). Python 3.10 runs
everything except the Sync service.
- [uv](https://docs.astral.sh/uv/) for dependency management
- Git

Expand All @@ -32,10 +33,19 @@ Or see the [uv installation guide](https://docs.astral.sh/uv/getting-started/ins
### Install dependencies

```bash
uv sync --group dev
uv sync --extra dev --extra prefect --extra service
```

This installs all runtime and development dependencies defined in `pyproject.toml`.
The `prefect` and `service` extras are not optional for development. Without them the type
checker cannot resolve the imports in `infrahub_sync/orchestration/` and
`infrahub_sync/service/`, and the tests that cover them skip themselves.

On Python 3.10 the Sync service is unavailable, so install the direct Prefect profile
instead and exclude the service from type checking:

```bash
uv sync --python 3.10 --extra dev --extra prefect
```

### Verify your setup

Expand All @@ -49,11 +59,16 @@ uv run infrahub-sync configs --help
Before committing any changes, run the following commands in order:

```bash
uv run invoke format # Format code with ruff
uv run invoke lint # Lint code with ruff and pylint
uv run mypy infrahub_sync/ --ignore-missing-imports
# Run `rumdl fmt .`, then Ruff formatting and safe fixes.
uv run invoke format
# Run `rumdl check .`, then Ruff, Pylint, yamllint, and ty; stop at the first failure.
uv run invoke lint
```

`invoke lint` stops after the first gate that fails. Pylint does not pass on a clean
checkout: its leg compares the run against a recorded baseline and fails only on a new
diagnostic code or a count above the recorded maximum.

### Validate the CLI

After making changes, verify the CLI still works:
Expand All @@ -66,22 +81,34 @@ uv run infrahub-sync runs plan --help

### Running tests

The offline gate is what a change has to keep green. It deselects the tests that need a
running stack or an external service:

```bash
uv run pytest -q
uv run pytest -m "not preview and not integration" -q
```

### Running the full stack locally

To run the Sync HTTP API, its Prefect worker, and a disposable Infrahub against your checkout, see the [local development stack](./development-stack.mdx).

The suite that exercises that stack is opt-in under the `preview` marker, and it skips
rather than fails when the stack is not running:

```bash
uv run invoke preview.up # start the stack
uv run invoke preview.smoke # seed, then run `pytest -m preview tests/preview`
uv run invoke preview.down --volumes
```

## Code standards

### Python style

- Python 3.10–3.13 compatible
- Type hints on new or changed code
- Ruff-formatted and lint-clean
- Mypy-checked (do not increase existing error count)
- Clean under `ty`; do not add `[[tool.ty.overrides]]` blocks to mask an error
- Public functions and classes require documentation strings
- Raise specific exceptions; avoid broad `except Exception:`

Expand All @@ -104,7 +131,7 @@ uv run invoke docs.generate
First-time setup (requires Node.js):

```bash
cd docs && npm install
cd docs && pnpm install --frozen-lockfile
```

Build the site:
Expand All @@ -115,9 +142,19 @@ uv run invoke docs.docusaurus

### Lint markdown files

Markdown structure is checked with [rumdl](https://github.com/rvben/rumdl), configured in
`pyproject.toml`:

```bash
uv run invoke docs.format-rumdl # run `rumdl fmt .`
uv run invoke docs.rumdl # run `rumdl check .`
```

Prose style is checked with [Vale](https://vale.sh/), configured in `.vale.ini`. Run it on
the files you changed:

```bash
npx markdownlint-cli "docs/docs/**/*.{md,mdx}"
npx markdownlint-cli --fix "docs/docs/**/*.{md,mdx}"
vale docs/docs/contributing.mdx
```

## Adding a new adapter
Expand Down Expand Up @@ -145,7 +182,10 @@ Common tasks:
| `linter.lint-ruff` | Lint Python code with ruff |
| `linter.lint-pylint` | Lint Python code with pylint |
| `linter.lint-yaml` | Lint YAML files with yamllint |
| `linter.lint-ty` | Type-check with ty |
| `docs.format-rumdl` | Format Markdown and MDX with rumdl |
| `docs.rumdl` | Lint Markdown and MDX with rumdl |
| `docs.generate` | Generate CLI documentation |
| `docs.docusaurus` | Build documentation website |
| `format` | Alias for ruff format |
| `lint` | Run all linters |
| `format` | Run rumdl formatting, then Ruff formatting and safe fixes |
| `lint` | Run rumdl, Ruff, Pylint, yamllint, and ty in order |
2 changes: 1 addition & 1 deletion docs/docs/reference/durable-product-records.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ that reports.
| `credential-path-not-declared` | A `$credential` reference sits somewhere that does not accept one. Usually a misspelled setting name, or a reference placed in `schema_mapping` or `order`. | the referencing node |
| `endpoint-not-absolute` | A `url` or `base_url` setting is not an absolute `http` or `https` URL. | the setting |
| `endpoint-not-relative` | An `api_endpoint` or `endpoint` setting carries a scheme or a host. It names a path beneath the absolute URL, not a second address. | the setting |
| `finding-limit-reached` | The package carries more than 256 defects and the rest were not reported. It is reported first, not last, and it is counted as a finding: when it fires the reported set holds 257 items, not 256. | the whole package |
| `finding-limit-reached` | The package carries more than 256 defects and the rest were not reported. It is reported first, not last, and it is counted as a finding: when it fires the reported set holds 257 items, not 256. Its severity is the highest one among the findings it stands for: `error` when any suppressed finding is an error, `warning` when they are all warnings — so a cut can neither hide an error nor invent one. | the whole package |
| `inline-credential-value` | A credential-bearing setting holds a literal value instead of a `{"$credential": "<name>"}` reference. A package never contains a credential value. | the setting |
| `malformed-credential-reference` | A credential declaration's environment identifier is not a valid variable name, or a `$credential` node carries keys beyond the reference itself. | the declaration or the node |
| `missing-adapter` | No adapter is installed under the declared name — check the spelling and the case. That role's settings are not judged at all, because there is no declared surface to judge them against, so expect exactly one finding for the role. | `/configuration/<role>` |
Expand Down
13 changes: 9 additions & 4 deletions docs/docs/tutorials/netbox-demo-to-infrahub.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,10 @@ export INFRAHUB_API_TOKEN="06438eb2-8019-4776-878c-0941b1f1d1ec"
```

Use the complete NetBox token created earlier. The worker, not the CLI, reads the NetBox
and Infrahub adapter credentials.
and Infrahub adapter credentials. The package declares them as references — `$credential`
entries under `credentials` naming `NETBOX_TOKEN` and `INFRAHUB_API_TOKEN` — so the worker
resolves the values from these variables. No NetBox or Infrahub adapter credential value is
written into the package or sent to the Sync API.

4. In terminal 2, create the process pool, deploy the service flow, and start its worker:

Expand Down Expand Up @@ -457,9 +460,11 @@ docker compose -f sync-services.yml down
ERROR | infrahub_sync.cli | Failed to initialize the Sync Instance: Error initializing InfrahubAdapter: Both url and token must be specified!
```

The service worker cannot find a NetBox or Infrahub adapter credential. Adapter credentials
belong in the worker environment, not the CLI environment or `config.yml`. Export these
variables before starting the Prefect worker:
The service worker cannot resolve a credential reference declared by the package. The
package and `config.yml` contain credential references and declarations, but secret
credential values belong only in the worker environment. The package names `NETBOX_TOKEN`
and `INFRAHUB_API_TOKEN`, and the worker reads their values. Export these variables before
starting the Prefect worker:

```bash
export NETBOX_URL="https://demo.netbox.dev"
Expand Down
10 changes: 8 additions & 2 deletions examples/netbox_to_infrahub/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ source:
name: netbox
settings:
url: "https://demo.netbox.dev"
token:
$credential: netbox-token

destination:
name: infrahub
settings:
url: "http://localhost:8000"
token:
$credential: infrahub-token

# Adapter credentials are read by the service worker, not by the CLI and not
# from this file:
# This file is the `configuration` body of package.yml, byte for byte, which is
# where the `credentials` block naming these two references lives. Each token is
# a reference, resolved by the service worker from its own environment. No
# credential value is ever written here. Export before starting the worker:
# export NETBOX_URL="https://demo.netbox.dev"
# export NETBOX_TOKEN="nbt_..."
# export INFRAHUB_ADDRESS="http://localhost:8000"
Expand Down
19 changes: 17 additions & 2 deletions examples/netbox_to_infrahub/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ configuration:
name: netbox
settings:
url: "https://demo.netbox.dev"
token:
$credential: netbox-token

destination:
name: infrahub
settings:
url: "http://localhost:8000"
token:
$credential: infrahub-token
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Adapter credentials are read by the service worker, not by the CLI and not
# from this file:
# Each token above is a reference, resolved by the service worker from its own
# environment. No credential value is ever written here, posted to the Sync
# API, or recorded in a registered version. Export before starting the worker:
# export NETBOX_URL="https://demo.netbox.dev"
# export NETBOX_TOKEN="nbt_..."
# export INFRAHUB_ADDRESS="http://localhost:8000"
Expand Down Expand Up @@ -673,3 +678,13 @@ configuration:
# - IpamSVLAN / IpamCVLAN (schemas/extensions/qinq): Infrahub-only
# concept (802.1ad Q-in-Q split), no equivalent Netbox source field.
# -----------------------------------------------------------------------

# What each `$credential` above points at. `env` is the installed provider; the
# identifier is the exact environment variable name the worker reads.
credentials:
netbox-token:
provider: env
identifier: NETBOX_TOKEN
infrahub-token:
provider: env
identifier: INFRAHUB_API_TOKEN
12 changes: 12 additions & 0 deletions tests/preview/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ def pytest_collection_modifyitems(items: list[Item]) -> None:
items[resume_at:resume_at] = observers


@pytest.fixture(scope="session")
def evidence_dir() -> Path:
"""Where the qualification rows write their captured HTTP transcripts.

Under the preview's own gitignored runtime state, next to the service logs, so one
directory holds everything a run of the matrix leaves behind for a reader.
"""
path = REPO_ROOT / ".preview" / "evidence"
path.mkdir(parents=True, exist_ok=True)
return path


@pytest.fixture(scope="session")
def preview_settings() -> dict[str, Any]:
"""Shipped-plus-local preview settings, with derived URLs and tokens.
Expand Down
Loading
Loading