diff --git a/.kiro/specs/admin-config-tool/tasks.md b/.kiro/specs/admin-config-tool/tasks.md new file mode 100644 index 00000000..86f645d2 --- /dev/null +++ b/.kiro/specs/admin-config-tool/tasks.md @@ -0,0 +1,142 @@ +# Implementation Plan + +- [ ] 1. Foundation: package scaffolding, shared errors, build/toolchain integration +- [x] 1.1 Set up the xceadmin Python package skeleton + - Editable install registers a real `xceadmin` console-script entry point backed by an empty, working click group (no subcommands yet — `config build`/`config push` are added by task 4.1, not this task) + - `xcecommon` is importable as a standalone package independent of `xceadmin`'s CLI + - Observable: running the entry point after an editable install prints usage/help text without error, showing zero commands + - _Requirements: 5.4_ + +- [ ] 1.2 Establish a shared error-handling foundation + - One common base exception type carries a human-readable message + - Every specific failure mode (schema not found, validation failure, reserved name, duplicate entry, connection timeout, corrupt reply, too many redirects, no primary available) has its own subclass + - Observable: raising any of these subclasses and catching the common base type recovers the message + - _Requirements: 7.1, 7.2, 7.3, 7.4_ + +- [ ] 1.3 Wire xceadmin into the project's build and dev-shell tooling + - Nix dev shell provides the Python interpreter and all dependencies needed to build and test xceadmin + - Running the build command for xceadmin performs an editable install by default and a pinned non-editable install under the release profile, running the test suite after whichever install just happened + - The build step keeps the packaged copies of the wire-protocol schema and the validation schemas in sync with their source locations elsewhere in the repo, and packages both correctly under either install mode + - Build/test command documentation, permission allowlist entries, and automatic formatting/linting on Python file edits all exist for the new component + - Observable: running the build command for this component from a clean checkout succeeds and reports test pass/fail + - _Requirements: 5.5, 8.1, 8.2, 8.6_ + +- [ ] 2. Core: xcecommon shared library +- [ ] 2.1 (P) Validate operator-authored YAML against the bundled schemas + - Each of the seven config file types can be checked against its own bundled validation schema, independent of anything the operator supplies + - Reserved queue/host names are rejected even when otherwise schema-valid + - Duplicate names within a single file/section are rejected + - Cross-references to undefined groups or resources are flagged as warnings without blocking the build + - Observable: validating a config directory with no schema files present anywhere in it still succeeds + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.5_ + - _Boundary: xcecommon.validation_ + +- [ ] 2.2 (P) Load the cluster wire-protocol schema + - The wire-protocol schema loads once per process and exposes every message type xceadmin needs to build or decode + - Loading works identically whether xceadmin was installed in editable or pinned mode + - A missing or corrupted schema copy produces a clear, actionable error rather than a crash + - Observable: loading the schema under a pinned (non-editable) install still resolves the bundled copy correctly + - _Requirements: 5.1, 5.5, 7.4_ + - _Boundary: xcecommon.schema_ + +- [ ] 2.3 (P) Map validated YAML into cluster configuration fields + - Every field the cluster configuration binary format defines is either populated from validated YAML or deliberately left untouched, with no field silently dropped + - List-based sections preserve the exact order they appeared in the YAML + - No timestamp, hostname, or username value ever enters the mapped output + - Building the same input twice produces identical output + - _Requirements: 1.1, 1.7, 1.8, 1.9, 1.10, 1.11, 1.12, 5.3, 9.1, 9.2, 9.3_ + - _Boundary: xcecommon.mapping_ + - _Depends: 2.2_ + +- [ ] 2.4 (P) Establish the TLS connection and wire framing to the controller + - A single request can be sent and a single reply received over a TLS connection using the cluster's existing certificate trust model + - Message framing matches the controller's own byte-for-byte, verified against known-good byte sequences + - The connection always closes cleanly, including on error or interruption + - _Requirements: 2.1, 2.3, 2.4, 5.2, 6.1, 7.1, 7.3, 7.6_ + - _Boundary: xcecommon.wire_ + - _Depends: 2.2_ + +- [ ] 3. Core: xceadmin CLI orchestration +- [ ] 3.1 (P) Compile a configs directory into a deployable cluster binary + - Running the build command against a directory of YAML files produces a binary output file with a printed checksum + - A missing individual YAML file is treated as an empty section, not an error + - Unrecognized-but-valid YAML fields produce a warning, not a failure + - Output can be directed to a specific path or to standard output + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.9, 8.3, 8.4_ + - _Boundary: xceadmin.build_ + - _Depends: 2.1, 2.3_ + +- [ ] 3.2 (P) Follow controller redirects to find the primary + - A reply pointing at a different host/port is followed automatically, up to a bounded number of hops + - Exceeding the hop limit or waiting past the connection timeout produces a clear error naming the last host contacted + - Observable: a chain of up to three redirects resolves to a final destination without operator intervention + - _Requirements: 3.1, 3.2, 3.3, 3.4_ + - _Boundary: xceadmin.discovery_ + - _Depends: 2.4_ + +- [ ] 3.3 Push a cluster binary to the controller, with a dry-run mode + - A real push reports the assigned configuration version, acknowledgement count, and any error, exiting non-zero on failure + - A dry-run reports whether the controller would accept the config without ever committing it + - Every connection failure, protocol-level rejection, corrupt input, or missing schema maps to a specific, distinct error + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_ + - _Boundary: xceadmin.push_ + - _Depends: 2.4, 3.2_ + +- [ ] 4. Integration: wire the CLI entry point and hook xceadmin into cluster deployment +- [ ] 4.1 Expose config build and config push as a single command-line tool + - `xceadmin config build` and `xceadmin config push` (with `--dry-run`) are both reachable from one installed command + - Any failure surfaces as a specific, human-readable message with a non-zero exit code, not a raw traceback + - Interrupting a push mid-flight leaves no dangling connection and reports a clean cancellation + - _Requirements: 1.2, 1.3, 2.2, 2.4, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 8.1, 8.2_ + - _Boundary: xceadmin.cli_ + - _Depends: 3.1, 3.3_ + +- [ ] 4.2 Build cluster configuration automatically during HA deployment + - Deploying a cluster with the build-config option produces a fresh binary from the current YAML before startup + - Deploying with no binary present and no flag still produces one automatically + - Deploying with an existing binary and no flag leaves it untouched, matching prior behavior + - _Requirements: 8.3, 8.4, 8.5_ + - _Boundary: deploy-ha.sh_ + - _Depends: 4.1_ + +- [ ] 5. Validation: test coverage across build, push, and packaging +- [ ] 5.1 (P) Unit test the field-mapping logic in isolation + - Both mapper shapes (list sections and the scalar section) are verified against in-memory input with no file I/O + - The one file that feeds two separate output fields is verified explicitly + - _Requirements: 1.1, 1.7, 1.8, 1.9, 1.10, 1.11, 1.12, 5.3, 9.1, 9.2, 9.3_ + - _Boundary: xcecommon.mapping_ + - _Depends: 2.3_ + +- [ ] 5.2 (P) Unit test wire framing + - Framing round-trips correctly against a fixed, known byte sequence and against a local loopback connection + - _Requirements: 2.1_ + - _Boundary: xcecommon.wire_ + - _Depends: 2.4_ + +- [ ] 5.3 (P) Verify bundled schemas resolve under every install mode + - Both the wire-protocol schema and the validation schemas resolve to real, parseable files under the currently active install + - A deliberately corrupted or absent packaged schema produces the expected installation-hint error rather than a crash + - Meaningful confirmation of the pinned install path requires running this under the release build profile + - _Requirements: 5.5, 7.4_ + - _Boundary: xcecommon.schema, xcecommon.validation_ + - _Depends: 2.1, 2.2, 1.3_ + +- [ ] 5.4 (P) Golden-file test config build determinism + - Building from a minimal fixture, a full fixture covering all seven file types, and a set of intentionally invalid fixtures each produce the expected result + - Building the same fixture twice produces byte-identical output and an identical checksum + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11, 1.12, 9.1, 9.2, 9.3_ + - _Boundary: xceadmin.build_ + - _Depends: 3.1_ + +- [ ] 5.5 (P) Test every YAML validation failure mode + - Each invalid-fixture case (schema violation, reserved name, duplicate name including a cross-type collision, undefined reference) produces the documented outcome — hard failure or warning, matching the requirement + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ + - _Boundary: xcecommon.validation_ + - _Depends: 2.1_ + +- [ ] 5.6 (P) Test config push and dry-run against a mock controller + - A mock TLS server standing in for the controller confirms the full push flow: successful push, redirect-following, and every documented error path + - The same mock confirms dry-run reports both an accepted and a rejected outcome correctly + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3_ + - _Boundary: xceadmin.push, xceadmin.discovery_ + - _Depends: 3.3_ diff --git a/src/xceadmin/pyproject.toml b/src/xceadmin/pyproject.toml new file mode 100644 index 00000000..c4bdf5ad --- /dev/null +++ b/src/xceadmin/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "xceadmin" +version = "0.1.0" +description = "XCE cluster configuration admin CLI (config build / config push)" +requires-python = ">=3.10" +dependencies = [ + "click>=8.0", +] + +# NOT wired into meson (src/meson.build / src/meson.options) — xceadmin is +# dispatched by src/tools/xce-build.sh's case-statement pattern instead. +# See .kiro/specs/admin-config-tool/design.md, File Structure Plan. + +[project.optional-dependencies] +test = ["pytest"] + +[project.scripts] +xceadmin = "xceadmin.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] +include = ["xceadmin*", "xcecommon*"] + +[tool.setuptools.package-dir] +"" = "src" diff --git a/src/xceadmin/src/xceadmin/__init__.py b/src/xceadmin/src/xceadmin/__init__.py new file mode 100644 index 00000000..5257608a --- /dev/null +++ b/src/xceadmin/src/xceadmin/__init__.py @@ -0,0 +1 @@ +"""xceadmin: CLI for building and deploying XCE cluster configuration.""" diff --git a/src/xceadmin/src/xceadmin/cli.py b/src/xceadmin/src/xceadmin/cli.py new file mode 100644 index 00000000..e47d09db --- /dev/null +++ b/src/xceadmin/src/xceadmin/cli.py @@ -0,0 +1,23 @@ +"""xceadmin command-line entry point. + +This module wires the ``xceadmin`` console-script entry point to a click +command group. It intentionally has zero subcommands for now — ``config +build`` and ``config push`` are added by a later task (see +.kiro/specs/admin-config-tool/tasks.md, task 4.1). +""" + +import click + + +@click.group() +def cli() -> None: + """xceadmin: build and deploy XCE cluster configuration.""" + + +def main() -> None: + """Console-script entry point (see pyproject.toml [project.scripts]).""" + cli() + + +if __name__ == "__main__": + main() diff --git a/src/xceadmin/src/xcecommon/__init__.py b/src/xceadmin/src/xcecommon/__init__.py new file mode 100644 index 00000000..813bf0ef --- /dev/null +++ b/src/xceadmin/src/xcecommon/__init__.py @@ -0,0 +1,9 @@ +"""xcecommon: shared library for XCE Python tooling. + +Provides capnp schema loading, YAML->capnp mapping, JSON Schema validation, +and the TLS wire protocol client shared by ``xceadmin`` and any future +Python client of the XCE controller wire protocol. + +This package has no dependency on ``click`` or any CLI-specific concern — +it must remain importable on its own, independent of ``xceadmin``. +""" diff --git a/src/xceadmin/tests/test_cli.py b/src/xceadmin/tests/test_cli.py new file mode 100644 index 00000000..8b797f3d --- /dev/null +++ b/src/xceadmin/tests/test_cli.py @@ -0,0 +1,44 @@ +"""Tests for the xceadmin package skeleton (task 1.1). + +Covers Requirement 5.4: xcecommon is importable as a standalone package +independent of xceadmin's CLI, and the xceadmin console-script entry point +is backed by a genuinely working (if currently empty) click group. +""" + +from click.testing import CliRunner + +from xceadmin.cli import cli + + +def test_cli_help_exits_zero() -> None: + """`xceadmin --help` must run cleanly and print usage text.""" + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + + assert result.exit_code == 0 + assert "Usage:" in result.output + + +def test_cli_help_shows_zero_commands() -> None: + """The click group has no subcommands yet (config build/push land later).""" + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + + assert result.exit_code == 0 + assert "Commands:" not in result.output + + +def test_cli_group_invoked_with_no_args_does_not_traceback() -> None: + """Invoking the bare group with no subcommand exits cleanly via click's + own SystemExit (no subcommand registered yet), not an unhandled + traceback from application code.""" + runner = CliRunner() + result = runner.invoke(cli, []) + + assert isinstance(result.exception, SystemExit) + assert "Usage:" in result.output + + +def test_xcecommon_is_importable_independent_of_xceadmin() -> None: + """xcecommon must be importable on its own, with no xceadmin import.""" + import xcecommon # noqa: F401 - import success is the assertion