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
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ jobs:
with:
context: .
# The Dockerfile lives under docker/; the context stays the repo
# root so COPY paths (src/, config/, tests/) are unchanged
# root so COPY paths (src/, tests/) are unchanged
file: docker/Dockerfile
# Only a v* tag publishes; PRs and main pushes build and throw away
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Changed

- **Breaking (who runs the image):** the image no longer bundles a demo configuration or connectors (`/app/config`, `/app/connectors`); mount your configuration at `/app/config` or set `CONFIG_DIR`, and take connectors from a project such as [unified-connectors](https://github.com/OpusProjects/unified-connectors), which now holds the SDK and the `kv` example.

## [0.1.0] - 2026-10-26

First version: GitOps for anything with an API.
Expand Down
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ src/
├── connectors/process.rs # ConnectorPort → script (scrubbed env, APPLICATION_CONFIG, timeout kills)
├── state/fs.rs # StateStorePort → <state.dir>/<application>/<kind>.yaml
├── git/, secrets/, directory/, config/, python.rs, process_env.rs
connectors/ # unified_connector.py (the Python SDK) + kv.py (example)
examples/desired-state/ # what the bundled config reconciles
tests/ # integration tests; tests/connectors/ holds test connector scripts
# (kv.py + the SDK, copied from unified-connectors)
```

## Architecture
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ the same spirit is welcome. Beyond those, comment only when the *why* is non-obv
cargo test # unit + integration; integration tests use the fixtures under tests/
```

The example connector (`connectors/kv.py`) and the test connectors under
`tests/connectors/` are plain Python — no external services are needed to run the
The test connectors under `tests/connectors/` (the `kv.py` example and its SDK among them) are
plain Python — no external services are needed to run the
full suite. See [docs/testing.md](docs/testing.md) for what the suite covers, running
a subset, and where new tests belong.
18 changes: 0 additions & 18 deletions config/applications/demo.yaml

This file was deleted.

25 changes: 0 additions & 25 deletions config/config.yaml

This file was deleted.

13 changes: 0 additions & 13 deletions config/connectors/kv.yaml

This file was deleted.

13 changes: 0 additions & 13 deletions config/projects/examples.yaml

This file was deleted.

15 changes: 6 additions & 9 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,17 @@ RUN apt-get update && apt-get install -y \

RUN groupadd -r unified && useradd -r -g unified -s /sbin/nologin unified

# Writable state directory: project checkouts, the local copy of each
# system's settings, and the demo store (the bundled config puts all three
# here). In k8s, mount a volume over it.
# Writable state directory: project checkouts and the local copy of each
# system's settings. In k8s, mount a volume over it.
RUN mkdir -p /var/lib/unified-gitops && chown unified:unified /var/lib/unified-gitops

COPY --from=builder /app/target/release/unified-gitops /usr/local/bin/unified-gitops
# Served at "/" when the config points server.ui.assets_dir here (the bundled
# config does).
# Served at "/" when the config points server.ui.assets_dir here.
COPY --from=ui /ui/apps/unified-gitops/dist /usr/share/unified-gitops/ui
COPY config/ /app/config/
# The connector SDK and the example connector, for connectors referenced by an
# absolute path (/app/connectors/...) rather than from a project checkout.
COPY connectors/ /app/connectors/

# No configuration is baked in: mount it at /app/config (the default
# CONFIG_DIR, relative to this WORKDIR) or point CONFIG_DIR elsewhere.
# Connectors come from projects (git checkouts), such as unified-connectors.
WORKDIR /app


Expand Down
15 changes: 9 additions & 6 deletions docs/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ code that handles the objects, once, the way a Kubernetes API server describes i

## The Python SDK

`connectors/unified_connector.py` implements the contract; a connector writes two functions:
The SDK, `unified_connector.py` at the root of
[unified-connectors](https://github.com/OpusProjects/unified-connectors), implements the contract.
A connector writes two functions:

```python
from unified_connector import ConnectorError, run
Expand All @@ -90,8 +92,8 @@ enforces the write guard, prints the
JSON and sets the exit code. Raise `ConnectorError("…")` for a one-line message; anything else prints a
traceback. Standard library only.

`connectors/kv.py` is a complete example: a key-value store kept as JSON files in a folder, with two
kinds (`namespace`, and `entry`, which depends on it).
`kv/kv.py` there is a complete example: a key-value store kept as JSON files in a folder, with two
kinds (`namespace`, and `entry`, which depends on it). This repository's tests run a copy of both.

## Declaring a connector

Expand Down Expand Up @@ -123,8 +125,9 @@ the write guard stops accidents in the script, and the system's own permissions
## Testing a connector by hand

```bash
# in a unified-connectors checkout
export APPLICATION_CONFIG='{"dir": "/tmp/store"}'
./connectors/kv.py describe
./connectors/kv.py list --kind namespace
echo '{"op":"create","object":{"name":"team-a"}}' | UNIFIED_ALLOW_WRITE=1 ./connectors/kv.py apply --kind namespace
./kv/kv.py describe
./kv/kv.py list --kind namespace
echo '{"op":"create","object":{"name":"team-a"}}' | UNIFIED_ALLOW_WRITE=1 ./kv/kv.py apply --kind namespace
```
4 changes: 2 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Deployment

The image is `ghcr.io/opusprojects/unified-gitops:<version>`, published from version tags only. It
holds the binary, the web UI, Python 3 with `requests`, `pyyaml` and `jinja2` for connectors, `git`, and
the bundled demo config in `/app/config`.
holds the binary, the web UI, Python 3 with `requests`, `pyyaml` and `jinja2` for connectors, and `git`.
It holds no configuration: mount yours at `/app/config` (the default `CONFIG_DIR`) or set `CONFIG_DIR`.

## Docker

Expand Down
4 changes: 2 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ directories for checkouts and state, and call the router as a service rather tha
| `domain/connector.rs` tests | described kinds: `depends_on` ordering, unknown kinds and cycles refused |
| `adapters/out/connectors/process.rs` tests | Scrubbed env, write flag only on apply, a hung connector is killed |
| `adapters/out/state/fs.rs` tests | The local copy reads back exactly (strings stay strings) |
| `tests/reconcile_test.rs` | The whole loop over HTTP with `connectors/kv.py`: refresh, sync in order, the local copy, drift, prune, invalid state touching nothing, restricted keys |
| `tests/reconcile_test.rs` | The whole loop over HTTP with `tests/connectors/kv.py`: refresh, sync in order, the local copy, drift, prune, invalid state touching nothing, restricted keys |
| `tests/config_api_test.rs` | The config directory over HTTP, staged validation, live reload |
| `tests/ldap_login_test.rs` | Sign-in, access rules, sessions, CSRF, throttling |
| the rest of `tests/` | git checkouts, Vault, metrics auth, UI assets, `--check-config` |
Expand All @@ -28,4 +28,4 @@ directories for checkouts and state, and call the router as a service rather tha
- A rule about desired state or the diff: a unit test next to it in `domain/`.
- Something a person or pipeline does over the API: `tests/reconcile_test.rs`, through the router.
- A connector's behaviour: test the connector script on its own (see docs/connectors.md); the core's tests
use `connectors/kv.py` and the two scripts in `tests/connectors/`.
use the scripts in `tests/connectors/` (`kv.py` and the SDK are copies from unified-connectors).
3 changes: 1 addition & 2 deletions docs/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ server:
assets_dir: "/usr/share/unified-gitops/ui" # where the image puts the build
```

The bundled `config/config.yaml` already has this block. `assets_dir` needs `enabled: true` (the UI
signs in through the login page) and is restart-only.
`assets_dir` needs `enabled: true` (the UI signs in through the login page) and is restart-only.

| Path | With a UI | Without |
| ----------------------------------------- | ------------------------------------------------- | -------------------- |
Expand Down
11 changes: 0 additions & 11 deletions examples/desired-state/demo/entries.yaml

This file was deleted.

7 changes: 0 additions & 7 deletions examples/desired-state/demo/namespaces.yaml

This file was deleted.

Binary file not shown.
2 changes: 2 additions & 0 deletions connectors/kv.py → tests/connectors/kv.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
"""Example connector: a key-value store kept as JSON files in a folder.

A copy of unified-connectors kv/kv.py, kept here so the tests run offline.

It stands in for a real system's API so the whole gitops loop can run on a
laptop: an application's config names the folder (`dir`), and each kind is one file
of objects keyed by `name`. Editing a file by hand is "someone changed the
Expand Down
2 changes: 1 addition & 1 deletion tests/connectors/slow_kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
if len(sys.argv) > 1 and sys.argv[1] == "apply":
time.sleep(1)
here = os.path.dirname(os.path.abspath(__file__))
kv = os.path.join(here, "..", "..", "connectors", "kv.py")
kv = os.path.join(here, "kv.py")
os.execv(sys.executable, [sys.executable, kv] + sys.argv[1:])
File renamed without changes.
4 changes: 2 additions & 2 deletions tests/reconcile_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use tower::ServiceExt;
use unified_gitops::adapters::r#in::http::auth::{Permissions, ResolvedApiKey};

// The whole loop through the real router and the real example connector
// (connectors/kv.py, a key-value store kept in a folder): desired state in a
// (tests/connectors/kv.py, a key-value store kept in a folder): desired state in a
// checkout, a check that reports what is out of sync, a sync that applies it,
// the local copy of the system's settings, and drift made by hand.

Expand All @@ -26,7 +26,7 @@ fn write(path: &Path, body: &str) {
}

fn fixture(prune: bool) -> Fixture {
fixture_with(prune, "connectors/kv.py")
fixture_with(prune, "tests/connectors/kv.py")
}

fn fixture_with(prune: bool, script: &str) -> Fixture {
Expand Down
4 changes: 2 additions & 2 deletions tests/rollback_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async fn rollback_applies_an_older_commit_and_pauses_until_the_next_sync() {
let new = commit(remote.path(), 5);

let connector: unified_gitops::domain::connector::Connector = serde_saphyr::from_str(&format!(
"path: {}/connectors/kv.py\n",
"path: {}/tests/connectors/kv.py\n",
env!("CARGO_MANIFEST_DIR")
))
.unwrap();
Expand Down Expand Up @@ -287,7 +287,7 @@ async fn failed_rollbacks_do_not_pause_and_checks_never_apply_over_one() {
let new = commit_root(remote.path(), "team-a", 5);

let connector: unified_gitops::domain::connector::Connector = serde_saphyr::from_str(&format!(
"path: {}/connectors/kv.py\n",
"path: {}/tests/connectors/kv.py\n",
env!("CARGO_MANIFEST_DIR")
))
.unwrap();
Expand Down
Loading