From 760271979062140580dc4d2776844ed078123499 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 15 Jul 2025 19:28:42 +0200 Subject: [PATCH] chore: initial commit --- .envrc | 2 + .gitignore | 5 + .gitlab-ci.yml | 4 + LICENSE.md | 7 + README.md | 65 +++++++ docs/examples.md | 84 ++++++++ docs/images/favicon.png | Bin 0 -> 1949 bytes docs/images/logo.png | Bin 0 -> 16252 bytes docs/index.md | 60 ++++++ docs/reference.md | 215 +++++++++++++++++++++ docs/usage.md | 119 ++++++++++++ examples/.sops.yaml | 2 + examples/sops.nix | 62 ++++++ flake.lock | 376 ++++++++++++++++++++++++++++++++++++ flake.nix | 215 +++++++++++++++++++++ lib/ansible-collections.nix | 45 +++++ lib/ansible-core.nix | 91 +++++++++ lib/default.nix | 20 ++ lib/flake.nix | 6 + lib/flakeModule.nix | 33 ++++ lib/module.nix | 103 ++++++++++ tests/cli_test.nix | 119 ++++++++++++ tests/integration_test.nix | 101 ++++++++++ tests/lib_test.nix | 182 +++++++++++++++++ 24 files changed, 1916 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 docs/examples.md create mode 100644 docs/images/favicon.png create mode 100644 docs/images/logo.png create mode 100644 docs/index.md create mode 100644 docs/reference.md create mode 100644 docs/usage.md create mode 100644 examples/.sops.yaml create mode 100644 examples/sops.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 lib/ansible-collections.nix create mode 100644 lib/ansible-core.nix create mode 100644 lib/default.nix create mode 100644 lib/flake.nix create mode 100644 lib/flakeModule.nix create mode 100644 lib/module.nix create mode 100644 tests/cli_test.nix create mode 100644 tests/integration_test.nix create mode 100644 tests/lib_test.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..23c1256 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use flake . --impure --accept-flake-config + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3dedc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.direnv +.devenv +result +.pre-commit-config.yaml + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..fce2c08 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,4 @@ +include: + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@2.1.0 + inputs: + version: 2.1.0 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4e58a63 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2025 TECHNOFAB + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..08b764c --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Nixible + +[![built with nix](https://img.shields.io/static/v1?logo=nixos&logoColor=white&label=&message=Built%20with%20Nix&color=41439a)](https://builtwithnix.org) +[![pipeline status](https://gitlab.com/TECHNOFAB/nixible/badges/main/pipeline.svg)](https://gitlab.com/TECHNOFAB/nixible/-/commits/main) +![License: MIT](https://img.shields.io/gitlab/license/technofab/nixible) +[![Latest Release](https://gitlab.com/TECHNOFAB/nixible/-/badges/release.svg)](https://gitlab.com/TECHNOFAB/nixible/-/releases) +[![Support me](https://img.shields.io/badge/Support-me-black)](https://tec.tf/#support) +[![Docs](https://img.shields.io/badge/Read-Docs-black)](https://nixible.projects.tf) + +A Nix-based tool for managing Ansible playbooks with type safety and reproducibility. + +## What is Nixible? + +Nixible bridges the Nix and Ansible ecosystems by allowing you to define Ansible playbooks, inventories, and collections as Nix expressions. It provides: + +- **Type-safe playbook definitions** using Nix's module system +- **Reproducible Ansible environments** with locked dependencies +- **Automatic collection management** from Ansible Galaxy + +## Quick Start + +### 1. Define your configuration + +Create a `some-playbook.nix` file: + +```nix title="some-playbook.nix" +{pkgs, ...}: { + collections = { + "community-general" = { + version = "8.0.0"; + hash = "sha256-..."; + }; + }; + + inventory = {}; # can also be omitted, we only use localhost + + playbook = [{ + name = "Hello World"; + hosts = "localhost"; + tasks = [{ + name = "Say hello"; + debug.msg = "Hello from Nixible!"; + }]; + }]; +} +``` + +### 2. Run with Nix + +```nix title="flake.nix" +{ + inputs.nixible.url = "gitlab:TECHNOFAB/nixible?dir=lib"; + # outputs = ... + # nixible_lib = inputs.nixible.lib { inherit pkgs lib; }; + packages.some-playbook = nixible_lib.mkNixibleCli ./some-playbook.nix; +} +``` + +```bash +nix run .#some-playbook +``` + +## Documentation + +Check the [docs](https://nixible.projects.tf). diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..d0cac20 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,84 @@ +# Examples + +See the `examples` directory in the repo. + +## Task Examples + +### File Operations + +```nix +{ + name = "Create configuration"; + template = { + src = "nginx.conf.j2"; + dest = "/etc/nginx/nginx.conf"; + backup = true; + }; + notify = "restart nginx"; +} +``` + +### Service Management + +```nix +{ + name = "Start services"; + service = { + name = "{{ item }}"; + state = "started"; + enabled = true; + }; + loop = ["nginx" "postgresql"]; +} +``` + +### Conditional Tasks + +```nix +{ + name = "Install SSL certificate"; + copy = { + src = "ssl/cert.pem"; + dest = "/etc/ssl/certs/"; + }; + when = "ssl_enabled | default(false)"; +} +``` + +### Block Tasks + +```nix +{ + block = [ + { + name = "Create user"; + user = { + name = "deploy"; + state = "present"; + }; + } + { + name = "Set up SSH key"; + authorized_key = { + user = "deploy"; + key = "{{ ssh_public_key }}"; + }; + } + ]; + rescue = [ + { + name = "Log error"; + debug.msg = "Failed to create user"; + } + ]; + always = [ + { + name = "Cleanup"; + file = { + path = "/tmp/setup"; + state = "absent"; + }; + } + ]; +} +``` diff --git a/docs/images/favicon.png b/docs/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4b380b107f726c3dc530ae70f44bb78fccfc16ce GIT binary patch literal 1949 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 Ee~vN_8~^|S literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..05f78c2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,60 @@ +# Introduction + +Nixible is a Nix-based tool for managing Ansible playbooks with type safety and reproducibility. + +## What is Nixible? + +Nixible bridges the Nix and Ansible ecosystems by allowing you to define Ansible playbooks, inventories, and collections as Nix expressions. It provides: + +- **Type-safe playbook definitions** using Nix's module system +- **Reproducible Ansible environments** with locked dependencies +- **Automatic collection management** from Ansible Galaxy + +## Quick Start + +### 1. Define your configuration + +Create a `some-playbook.nix` file: + +```nix title="some-playbook.nix" +{pkgs, ...}: { + collections = { + "community-general" = { + version = "8.0.0"; + hash = "sha256-..."; + }; + }; + + inventory = {}; # can also be omitted, we only use localhost + + playbook = [{ + name = "Hello World"; + hosts = "localhost"; + tasks = [{ + name = "Say hello"; + debug.msg = "Hello from Nixible!"; + }]; + }]; +} +``` + +### 2. Run with Nix + +```nix title="flake.nix" +{ + inputs.nixible.url = "gitlab:TECHNOFAB/nixible?dir=lib"; + # outputs = ... + # nixible_lib = inputs.nixible.lib { inherit pkgs lib; }; + packages.some-playbook = nixible_lib.mkNixibleCli ./some-playbook.nix; +} +``` + +```bash +nix run .#some-playbook +``` + +## Getting Started + +1. **[Usage](usage.md)** - Learn how to build and run Nixible configurations +1. **[Examples](examples.md)** - See real-world usage patterns +1. **[Reference](reference.md)** - Detailed API and configuration reference diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..3ef3160 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,215 @@ +# Reference + +## `flakeModule` + +The `flakeModule` for [flake-parts](https://flake.parts). + +Provides a `perSystem.nixible` option for defining Nixible configurations directly in your flake. + +```nix +{ + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixible.url = "gitlab:TECHNOFAB/nixible?dir=lib"; + }; + + outputs = inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ nixible.flakeModule ]; + systems = # ... + + perSystem = { pkgs, ... }: { + nixible = { + "deploy" = { + dependencies = [ pkgs.rsync ]; + playbook = [{ + name = "Deploy application"; + hosts = "servers"; + tasks = [ /* ... */ ]; + }]; + }; + "backup" = { + dependencies = [ pkgs.borg ]; + playbook = [{ + name = "Backup data"; + hosts = "backup_servers"; + tasks = [ /* ... */ ]; + }]; + }; + }; + }; + }; +} +``` + +Each configuration defined in `perSystem.nixible` automatically creates a corresponding package in `legacyPackages` with the name `nixible:`. These packages contain the CLI executable for that specific configuration. + +**Example usage:** + +```bash +nix run .#nixible:deploy +nix run .#nixible:backup +``` + +## `lib` + +### `module` + +The nix module for validation of Nixible configurations. +Used internally by `mkNixible`. + +### `mkNixible` + +```nix +mkNixible config +``` + +Creates a Nixible configuration module evaluation. +`config` can be a path to a nix file or a function/attrset. + +**Noteworthy attributes**: + +- `config`: The evaluated configuration with all options +- `config.inventoryFile`: Generated JSON inventory file +- `config.playbookFile`: Generated YAML playbook file +- `config.installedCollections`: Directory containing installed collections +- `config.cli`: The nixible CLI executable + +### `mkNixibleCli` + +```nix +mkNixibleCli config +``` + +Creates a CLI executable for your Nixible configuration. +Basically `(mkNixible config).config.cli`. + +## Configuration Options + +### `ansiblePackage` + +**Type:** `package` +**Default:** Custom ansible-core package + +The Ansible package to use. The default package is optimized for size, by not +including the gazillion collections that `pkgs.ansible` and `pkgs.ansible-core` include. + +```nix +ansiblePackage = pkgs.ansible; +``` + +### `collections` + +**Type:** `attrsOf collectionType` +**Default:** `{}` + +Ansible collections to fetch from Galaxy. + +```nix +collections = { + "community-general" = { + version = "8.0.0"; + hash = "sha256-..."; + }; +}; +``` + +### `dependencies` + +**Type:** `listOf package` +**Default:** `[]` + +Additional packages available at runtime. + +```nix +dependencies = [pkgs.git pkgs.rsync]; +``` + +### `inventory` + +**Type:** `attrs` +**Default:** `{}` + +Ansible inventory as Nix data structure, converted to JSON. + +```nix +inventory = { + webservers = { + hosts = { + web1 = { ansible_host = "192.168.1.10"; }; + }; + vars = { + http_port = 80; + }; + }; +}; +``` + +### `playbook` + +**Type:** `listOf playbookType` + +List of plays that make up the playbook. + +```nix +playbook = [ + { + name = "Configure servers"; + hosts = "webservers"; + become = true; + tasks = [ + { + name = "Install nginx"; + package = { + name = "nginx"; + state = "present"; + }; + } + ]; + } +]; +``` + +## Collection Type + +### `version` + +**Type:** `str` + +Version of the collection from Ansible Galaxy. + +### `hash` + +**Type:** `str` + +SHA256 hash of the collection tarball for verification. + +## Playbook Type + +### `name` + +**Type:** `str` + +Name of the play. + +### `hosts` + +**Type:** `str` + +Target hosts pattern (e.g., "all", "webservers", "localhost"). + +### `become` + +**Type:** `bool` +**Default:** `false` + +Whether to use privilege escalation. + +### `tasks` + +**Type:** `listOf attrs` +**Default:** `[]` + +List of tasks to execute. Each task corresponds to Ansible task syntax. + +Standard Ansible playbook options are supported: `gather_facts`, `serial`, `vars`, `vars_files`, `tags`, `handlers`, `pre_tasks`, `post_tasks`, etc. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..d93976e --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,119 @@ +# Usage + +Learn how to build and use Nixible configurations. + +## Using flakeModule + +The recommended way to use Nixible is with the flakeModule: + +```nix title="flake.nix" +{ + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixible.url = "gitlab:TECHNOFAB/nixible?dir=lib"; + }; + + outputs = inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ nixible.flakeModule ]; + systems = # ... + + perSystem = { pkgs, ... }: { + nixible = { + deploy = { + dependencies = [ pkgs.rsync ]; + inventory = { + webservers = { + hosts = { + web1 = { ansible_host = "192.168.1.10"; }; + }; + }; + }; + playbook = [{ + name = "Deploy application"; + hosts = "webservers"; + tasks = [{ + name = "Deploy files"; + copy = { + src = "{{ pwd }}/dist/"; + dest = "/var/www/"; + }; + }]; + }]; + }; + }; + }; + }; + }; + }; +} +``` + +Then run with: + +```bash +nix run .#nixible:deploy + +# With ansible-playbook options +nix run .#nixible:deploy -- --check --diff --limit web1 +``` + +## Using the CLI directly + +You can also create CLI packages directly: + +```nix title="flake.nix" +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + nixible.url = "gitlab:TECHNOFAB/nixible?dir=lib"; + }; + + outputs = { nixpkgs, nixible, ... }: let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + lib = nixpkgs.lib; + nixible_lib = nixible.lib { inherit pkgs lib; }; + in { + packages.x86_64-linux.deploy = nixible_lib.mkNixibleCli ./deploy.nix; + }; +} +``` + +Then run with: + +```bash +nix run .#deploy + +# Dry run with diff +nix run .#deploy -- --check --diff + +# Limit to specific hosts +nix run .#deploy -- --limit webservers + +# Extra variables +nix run .#deploy -- --extra-vars "env=production debug=true" +# etc. +``` + +## Variables + +Nixible automatically provides these variables to your playbooks: + +- `pwd`: Current working directory when nixible is run +- `git_root`: Git repository root (empty if not in a git repo) + +Use them in your playbooks: + +```nix +playbook = [{ + name = "Deploy from current directory"; + hosts = "localhost"; + tasks = [{ + name = "Copy files"; + copy = { + src = "{{ pwd }}/dist/"; + dest = "/var/www/"; + }; + }]; +}]; +``` diff --git a/examples/.sops.yaml b/examples/.sops.yaml new file mode 100644 index 0000000..69d0a16 --- /dev/null +++ b/examples/.sops.yaml @@ -0,0 +1,2 @@ +keys: [] +creation_rules: [] diff --git a/examples/sops.nix b/examples/sops.nix new file mode 100644 index 0000000..180c04a --- /dev/null +++ b/examples/sops.nix @@ -0,0 +1,62 @@ +{pkgs, ...}: { + # + # NOTE: needs a .sops.yaml file in the directory to work + # + dependencies = [pkgs.sops]; + + collections = { + "community-crypto" = { + version = "3.0.0"; + hash = "sha256-sRuv2qateLgZRWlTtHO1f2hb4vb7Oc/2DHTuLmexuiI="; + }; + "community-sops" = { + version = "2.1.0"; + hash = "sha256-5VGVBV+z4bUe6XdKu5P8+HbABCvgeR8hvDmL5s1BfUM="; + }; + }; + + playbook = [ + { + name = "Create SOPS-encrypted private key"; + hosts = "localhost"; + tasks = [ + { + block = [ + { + name = "Create private key"; + "community.crypto.openssl_privatekey_pipe" = { + size = 2048; + content = + # jinja + '' + {{ lookup( + 'community.sops.sops', + "{{ pwd }}/keys/private_key.pem.sops", + config_path='${./.sops.yaml}', + empty_on_not_exist=true) }} + ''; + }; + no_log = true; + register = "private_key"; + } + { + name = "Write encrypted key to disk"; + when = "private_key is changed"; + "community.sops.sops_encrypt" = { + path = "{{ pwd }}/keys/private_key.pem.sops"; + content_text = "{{ private_key.privatekey }}"; + config_path = ./.sops.yaml; + }; + } + ]; + always = [ + { + name = "Wipe private key from Ansible's facts"; + set_fact.private_key = ""; + } + ]; + } + ]; + } + ]; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..15209f4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,376 @@ +{ + "nodes": { + "cachix": { + "inputs": { + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv" + ], + "git-hooks": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748883665, + "narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=", + "owner": "cachix", + "repo": "cachix", + "rev": "f707778d902af4d62d8dd92c269f8e70de09acbe", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", + "nix": "nix", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1752600380, + "narHash": "sha256-3ZLDE0Taf9fqcTK6+fhDvq06WgzudK/E70zdddSc5vA=", + "owner": "cachix", + "repo": "devenv", + "rev": "7441c97330233543ee28d5c4612173f108250536", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1751413152, + "narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "77826244401ea9de6e3bac47c2db46005e1f30b5", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1750779888, + "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "mkdocs-material-umami": { + "locked": { + "lastModified": 1745840856, + "narHash": "sha256-1Ad1JTMQMP6YsoIKAA+SBCE15qWrYkGue9/lXOLnu9I=", + "owner": "technofab", + "repo": "mkdocs-material-umami", + "rev": "3ac9b194450f6b779c37b8d16fec640198e5cd0a", + "type": "gitlab" + }, + "original": { + "owner": "technofab", + "repo": "mkdocs-material-umami", + "type": "gitlab" + } + }, + "nix": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "flake-parts": "flake-parts", + "git-hooks-nix": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ] + }, + "locked": { + "lastModified": 1752251701, + "narHash": "sha256-fkkkwB7jz+14ZdIHAYCCNypO9EZDCKpj7LEQZhV6QJs=", + "owner": "cachix", + "repo": "nix", + "rev": "54df04f09cb084b9e58529c0ae6f53f0e50f1a19", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "devenv-2.30", + "repo": "nix", + "type": "github" + } + }, + "nix-gitlab-ci": { + "locked": { + "dir": "lib", + "lastModified": 1752052838, + "narHash": "sha256-EqP4xB8YTVXWPCCchnVtQbuq0bKa79TUEcPF3hjuX/k=", + "owner": "technofab", + "repo": "nix-gitlab-ci", + "rev": "0c6949f585a2c1ea2cf85fc01445496f7c75faae", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "technofab", + "repo": "nix-gitlab-ci", + "type": "gitlab" + } + }, + "nix-mkdocs": { + "locked": { + "dir": "lib", + "lastModified": 1745841841, + "narHash": "sha256-297zPQbUlc7ZAYDoaD6mCmQxCC3Tr4YOKekRF1ArZ7g=", + "owner": "technofab", + "repo": "nixmkdocs", + "rev": "c7e3c3b13ded25818e9789938387bba6f2cde690", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "technofab", + "repo": "nixmkdocs", + "type": "gitlab" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1750441195, + "narHash": "sha256-yke+pm+MdgRb6c0dPt8MgDhv7fcBbdjmv1ZceNTyzKg=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "0ceffe312871b443929ff3006960d29b120dc627", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1751159883, + "narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1752446735, + "narHash": "sha256-Nz2vtUEaRB/UjvPfuhHpez060P/4mvGpXW4JCDIboA4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a421ac6595024edcfbb1ef950a3712b89161c359", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1747958103, + "narHash": "sha256-qmmFCrfBwSHoWw7cVK4Aj+fns+c54EBP8cGqp/yK410=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fe51d34885f7b5e3e7b59572796e1bcb427eccb1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixtest": { + "locked": { + "dir": "lib", + "lastModified": 1749915293, + "narHash": "sha256-SeDdPVcvtgkBK1fb8lLKf+1iOY8UyDTVN6A6H19aw2M=", + "owner": "technofab", + "repo": "nixtest", + "rev": "c2a1208534fbdd8ab28ff3e45262b527f81a1755", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "technofab", + "repo": "nixtest", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "flake-parts": "flake-parts_2", + "mkdocs-material-umami": "mkdocs-material-umami", + "nix-gitlab-ci": "nix-gitlab-ci", + "nix-mkdocs": "nix-mkdocs", + "nixpkgs": "nixpkgs_2", + "nixtest": "nixtest", + "systems": "systems", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1752055615, + "narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7e10acf --- /dev/null +++ b/flake.nix @@ -0,0 +1,215 @@ +{ + outputs = { + flake-parts, + systems, + ... + } @ inputs: + flake-parts.lib.mkFlake {inherit inputs;} { + imports = [ + inputs.devenv.flakeModule + inputs.treefmt-nix.flakeModule + inputs.nix-gitlab-ci.flakeModule + inputs.nix-mkdocs.flakeModule + ./lib/flakeModule.nix + ]; + systems = import systems; + flake = {}; + perSystem = { + lib, + pkgs, + config, + ... + }: { + treefmt = { + projectRootFile = "flake.nix"; + programs = { + alejandra.enable = true; + mdformat.enable = true; + }; + }; + devenv.shells.default = { + containers = pkgs.lib.mkForce {}; + + git-hooks.hooks = { + treefmt = { + enable = true; + packageOverrides.treefmt = config.treefmt.build.wrapper; + }; + convco.enable = true; + }; + }; + doc = { + path = ./docs; + deps = pp: [ + pp.mkdocs-material + (pp.callPackage inputs.mkdocs-material-umami {}) + ]; + config = { + site_name = "Nixible"; + repo_name = "TECHNOFAB/nixible"; + repo_url = "https://gitlab.com/TECHNOFAB/nixible"; + edit_uri = "edit/main/docs/"; + theme = { + name = "material"; + features = ["content.code.copy" "content.action.edit"]; + icon.repo = "simple/gitlab"; + logo = "images/logo.png"; + favicon = "images/favicon.png"; + palette = [ + { + scheme = "default"; + media = "(prefers-color-scheme: light)"; + primary = "black"; + accent = "blue"; + toggle = { + icon = "material/brightness-7"; + name = "Switch to dark mode"; + }; + } + { + scheme = "slate"; + media = "(prefers-color-scheme: dark)"; + primary = "black"; + accent = "blue"; + toggle = { + icon = "material/brightness-4"; + name = "Switch to light mode"; + }; + } + ]; + }; + plugins = ["search" "material-umami"]; + nav = [ + {"Introduction" = "index.md";} + {"Usage" = "usage.md";} + {"Examples" = "examples.md";} + {"Reference" = "reference.md";} + ]; + markdown_extensions = [ + "pymdownx.superfences" + "admonition" + ]; + extra.analytics = { + provider = "umami"; + site_id = "d8354dfa-2ad2-4089-90d2-899b981aef22"; + src = "https://analytics.tf/umami"; + domains = "nixible.projects.tf"; + feedback = { + title = "Was this page helpful?"; + ratings = [ + { + icon = "material/thumb-up-outline"; + name = "This page is helpful"; + data = "good"; + note = "Thanks for your feedback!"; + } + { + icon = "material/thumb-down-outline"; + name = "This page could be improved"; + data = "bad"; + note = "Thanks for your feedback! Please leave feedback by creating an issue :)"; + } + ]; + }; + }; + }; + }; + ci = { + stages = ["test" "build" "deploy"]; + jobs = { + "test:lib" = { + stage = "test"; + script = [ + "nix run .#tests -- --junit=junit.xml" + ]; + allow_failure = true; + artifacts = { + when = "always"; + reports.junit = "junit.xml"; + }; + }; + "docs" = { + stage = "build"; + script = [ + # sh + '' + nix build .#docs:default + mkdir -p public + cp -r result/. public/ + '' + ]; + artifacts.paths = ["public"]; + }; + "pages" = { + nix.enable = false; + image = "alpine:latest"; + stage = "deploy"; + script = ["true"]; + artifacts.paths = ["public"]; + rules = [ + { + "if" = "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"; + } + ]; + }; + }; + }; + + nixible = { + "hello".playbook = [ + { + name = "Hello World"; + hosts = "localhost"; + tasks = [ + { + name = "Say hello"; + debug.msg = "Hello from Nixible!"; + } + ]; + } + ]; + "another".playbook = []; + }; + + packages = let + nblib = import ./lib {inherit pkgs lib;}; + ntlib = inputs.nixtest.lib {inherit pkgs lib;}; + in { + tests = ntlib.mkNixtest { + modules = ntlib.autodiscover {dir = ./tests;}; + args = { + inherit pkgs nblib ntlib; + }; + }; + }; + }; + }; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + # flake & devenv related + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default-linux"; + devenv.url = "github:cachix/devenv"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + nix-gitlab-ci.url = "gitlab:technofab/nix-gitlab-ci?dir=lib"; + nixtest.url = "gitlab:technofab/nixtest?dir=lib"; + nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib"; + mkdocs-material-umami.url = "gitlab:technofab/mkdocs-material-umami"; + }; + + nixConfig = { + extra-substituters = [ + "https://cache.nixos.org/" + "https://nix-community.cachix.org" + "https://devenv.cachix.org" + ]; + + extra-trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=" + ]; + }; +} diff --git a/lib/ansible-collections.nix b/lib/ansible-collections.nix new file mode 100644 index 0000000..ac362e1 --- /dev/null +++ b/lib/ansible-collections.nix @@ -0,0 +1,45 @@ +{ + stdenv, + lib, + pkgs, +}: ansible: collections: let + inherit (lib) concatStringsSep mapAttrsToList; + + mkCollection = { + name, + version, + hash, + }: + stdenv.mkDerivation { + pname = name; + inherit version; + src = pkgs.fetchurl { + inherit hash; + url = "https://galaxy.ansible.com/download/${name}-${version}.tar.gz"; + }; + + phases = ["installPhase"]; + + installPhase = '' + mkdir -p $out + cp $src $out/collection.tar.gz + ''; + }; + + installCollection = collection: "${ansible}/bin/ansible-galaxy collection install ${collection}/collection.tar.gz"; + installCollections = concatStringsSep "\n" ( + mapAttrsToList ( + name: coll: + installCollection ( + mkCollection ({inherit name;} // coll) + ) + ) + collections + ); +in + pkgs.runCommand "ansible-collections" {} '' + mkdir -p $out + export HOME=./ + export ANSIBLE_COLLECTIONS_PATH=$out + ${installCollections} + '' diff --git a/lib/ansible-core.nix b/lib/ansible-core.nix new file mode 100644 index 0000000..85c7de1 --- /dev/null +++ b/lib/ansible-core.nix @@ -0,0 +1,91 @@ +{ + lib, + buildPythonPackage, + fetchPypi, + installShellFiles, + docutils, + setuptools, + cryptography, + jinja2, + junit-xml, + lxml, + ncclient, + packaging, + paramiko, + ansible-pylibssh, + pexpect, + psutil, + pycrypto, + pyyaml, + requests, + resolvelib, + scp, + windowsSupport ? false, + pywinrm, + xmltodict, +}: +buildPythonPackage rec { + pname = "ansible-core"; + version = "2.18.6"; + pyproject = true; + + src = fetchPypi { + pname = "ansible_core"; + inherit version; + hash = "sha256-JbsgzhUWobcweDGyY872hAQ7NyBxFGa9nUFk5f1XZVc="; + }; + + # ansible_connection is already wrapped, so don't pass it through + # the python interpreter again, as it would break execution of + # connection plugins. + postPatch = '' + substituteInPlace lib/ansible/executor/task_executor.py \ + --replace "[python," "[" + + patchShebangs --build packaging/cli-doc/build.py + + SETUPTOOLS_PATTERN='"setuptools[0-9 <>=.,]+"' + PYPROJECT=$(cat pyproject.toml) + if [[ "$PYPROJECT" =~ $SETUPTOOLS_PATTERN ]]; then + echo "setuptools replace: ''${BASH_REMATCH[0]}" + echo "''${PYPROJECT//''${BASH_REMATCH[0]}/'"setuptools"'}" > pyproject.toml + else + exit 2 + fi + ''; + + nativeBuildInputs = [ + installShellFiles + docutils + ]; + + build-system = [setuptools]; + + dependencies = + [ + # from requirements.txt + cryptography + jinja2 + packaging + pyyaml + resolvelib + # optional dependencies + junit-xml + lxml + ncclient + paramiko + ansible-pylibssh + pexpect + psutil + pycrypto + requests + scp + xmltodict + ] + ++ lib.optionals windowsSupport [pywinrm]; + + pythonRelaxDeps = ["resolvelib"]; + + # internal import errors, missing dependencies + doCheck = false; +} diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..1869977 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,20 @@ +{ + pkgs, + lib ? pkgs.lib, + ... +}: let + inherit (lib) evalModules; +in rec { + module = ./module.nix; + + mkNixible = config: + evalModules { + specialArgs = {inherit pkgs;}; + modules = [ + module + config + ]; + }; + + mkNixibleCli = config: (mkNixible config).config.cli; +} diff --git a/lib/flake.nix b/lib/flake.nix new file mode 100644 index 0000000..ac97b82 --- /dev/null +++ b/lib/flake.nix @@ -0,0 +1,6 @@ +{ + outputs = {...}: { + lib = import ./.; + flakeModule = ./flakeModule.nix; + }; +} diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix new file mode 100644 index 0000000..fd2b517 --- /dev/null +++ b/lib/flakeModule.nix @@ -0,0 +1,33 @@ +{ + flake-parts-lib, + lib, + ... +}: let + inherit (lib) mkOption types; +in { + options.perSystem = flake-parts-lib.mkPerSystemOption ( + { + config, + pkgs, + ... + }: let + nixible-lib = import ./. {inherit pkgs lib;}; + in { + options.nixible = mkOption { + type = types.attrsOf (types.submodule (args: + # needed to get pkgs in there, weirdly enough + import nixible-lib.module (args + // { + inherit pkgs; + }))); + default = {}; + }; + + config.legacyPackages = lib.fold (playbook: acc: acc // playbook) {} ( + map (playbook_name: { + "nixible:${playbook_name}" = (builtins.getAttr playbook_name config.nixible).cli; + }) (builtins.attrNames config.nixible) + ); + } + ); +} diff --git a/lib/module.nix b/lib/module.nix new file mode 100644 index 0000000..4833971 --- /dev/null +++ b/lib/module.nix @@ -0,0 +1,103 @@ +{ + lib, + pkgs, + config, + ... +}: let + inherit (lib) types mkOption; + + collectionType = types.submodule { + options = { + version = mkOption { + type = types.str; + description = "Version of collection"; + }; + hash = mkOption { + type = types.str; + description = "Hash of the collection tarball"; + }; + }; + }; +in { + options = { + ansiblePackage = mkOption { + type = types.package; + default = pkgs.python3Packages.callPackage ./ansible-core.nix {}; + description = "Ansible package to use (default doesn't have any collections installed for size)"; + }; + collections = mkOption { + type = types.attrsOf collectionType; + default = {}; + description = "Collections to fetch and install"; + }; + dependencies = mkOption { + type = types.listOf types.package; + default = []; + description = "List of packages to include at runtime"; + }; + playbook = mkOption { + type = types.listOf (types.submodule { + options = { + name = mkOption { + type = types.str; + description = "Name of the play"; + }; + hosts = mkOption { + type = types.str; + description = "The target hosts for this play (e.g., 'all', 'webservers')"; + }; + become = mkOption { + type = types.bool; + default = false; + description = "Whether to use privilege escalation (become: yes)"; + }; + tasks = mkOption { + type = types.listOf types.attrs; + default = []; + description = "List of tasks to execute in this play"; + }; + }; + }); + description = "The actual playbook, defined as a Nix data structure"; + }; + + inventory = mkOption { + type = types.attrs; + default = {}; + description = "Ansible inventory, will be converted to json and passed to ansible"; + }; + + inventoryFile = mkOption { + internal = true; + type = types.package; + }; + playbookFile = mkOption { + internal = true; + type = types.package; + }; + installedCollections = mkOption { + internal = true; + type = types.package; + }; + cli = mkOption { + internal = true; + type = types.package; + }; + }; + config = { + inventoryFile = (pkgs.formats.json {}).generate "inventory.json" config.inventory; + playbookFile = (pkgs.formats.yaml {}).generate "playbook.yml" config.playbook; + installedCollections = pkgs.callPackage ./ansible-collections.nix {} config.ansiblePackage config.collections; + cli = pkgs.writeShellApplication { + name = "nixible"; + runtimeInputs = config.dependencies; + text = '' + set -euo pipefail + export ANSIBLE_COLLECTIONS_PATH=${config.installedCollections} + + git_repo=$(git rev-parse --show-toplevel 2>/dev/null || true) + ${config.ansiblePackage}/bin/ansible-playbook -i ${config.inventoryFile} ${config.playbookFile} -e "pwd=$(pwd)" -e "git_root=$git_repo" "$@" + ''; + }; + }; +} diff --git a/tests/cli_test.nix b/tests/cli_test.nix new file mode 100644 index 0000000..06ec865 --- /dev/null +++ b/tests/cli_test.nix @@ -0,0 +1,119 @@ +{ + pkgs, + nblib, + ntlib, + ... +}: { + suites."CLI Tests" = { + pos = __curPos; + tests = [ + { + name = "dependencies inclusion"; + type = "script"; + script = let + config = {pkgs, ...}: { + dependencies = [pkgs.git pkgs.curl]; + playbook = [ + { + name = "Test dependencies"; + hosts = "localhost"; + tasks = []; + } + ]; + }; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + # check that dependencies are included in runtime inputs + assert_file_contains "${cli}/bin/nixible" "${pkgs.git}" "should include git in PATH" + assert_file_contains "${cli}/bin/nixible" "${pkgs.curl}" "should include curl in PATH" + ''; + } + { + name = "CLI executable structure"; + type = "script"; + script = let + config = {pkgs, ...}: { + dependencies = [pkgs.git]; + playbook = [ + { + name = "CLI test"; + hosts = "localhost"; + tasks = [ + { + debug.msg = "Testing CLI"; + } + ]; + } + ]; + }; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + # check CLI is executable + assert "-x ${cli}/bin/nixible" "CLI should be executable" + + # check wrapper content + assert_file_contains "${cli}/bin/nixible" "set -euo pipefail" "should have error handling" + assert_file_contains "${cli}/bin/nixible" "ansible-playbook" "should call ansible-playbook" + assert_file_contains "${cli}/bin/nixible" "git rev-parse --show-toplevel" "should detect git repo" + ''; + } + { + name = "variables setup"; + type = "script"; + script = let + config = { + playbook = [ + { + name = "Environment test"; + hosts = "localhost"; + tasks = []; + } + ]; + }; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + assert_file_contains "${cli}/bin/nixible" 'export ANSIBLE_COLLECTIONS_PATH=' "should export collections path" + assert_file_contains "${cli}/bin/nixible" '-e "pwd=$(pwd)"' "should pass pwd variable" + assert_file_contains "${cli}/bin/nixible" '-e "git_root=$git_repo"' "should pass git_root variable" + ''; + } + { + name = "runtime dependencies inclusion"; + type = "script"; + script = let + config = {pkgs, ...}: { + dependencies = [pkgs.rsync pkgs.openssh]; + playbook = [ + { + name = "Dependencies test"; + hosts = "localhost"; + tasks = []; + } + ]; + }; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + # check runtime dependencies are properly included + assert_file_contains "${cli}/bin/nixible" "rsync" "should include rsync from runtimeInputs" + assert_file_contains "${cli}/bin/nixible" "openssh" "should include openssh from runtimeInputs" + ''; + } + ]; + }; +} diff --git a/tests/integration_test.nix b/tests/integration_test.nix new file mode 100644 index 0000000..ec49be5 --- /dev/null +++ b/tests/integration_test.nix @@ -0,0 +1,101 @@ +{ + pkgs, + nblib, + ntlib, + ... +}: { + suites."Integration Tests" = { + pos = __curPos; + tests = [ + { + name = "end-to-end configuration processing"; + type = "script"; + script = let + config = {pkgs, ...}: { + dependencies = [pkgs.curl]; + collections = { + "community-general" = { + version = "8.0.0"; + hash = "sha256-dNtdCxGj72LfMqPfzOpUSXLNLj1IkaAewRmHNizh67Q="; + }; + }; + inventory = { + test_group = { + hosts = { + test1 = {ansible_host = "localhost";}; + }; + vars = { + test_var = "test_value"; + }; + }; + }; + playbook = [ + { + name = "End-to-end test"; + hosts = "test_group"; + become = false; + tasks = [ + { + name = "Test task"; + debug = { + msg = "Hello from {{ inventory_hostname }}"; + var = "test_var"; + }; + } + ]; + } + ]; + }; + result = nblib.mkNixible config; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.path [pkgs.jq pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + + # test that all components are generated + assert "-f ${result.config.inventoryFile}" "should generate inventory file" + assert "-f ${result.config.playbookFile}" "should generate playbook file" + assert "-d ${result.config.installedCollections}" "should create collections directory" + assert "-x ${cli}/bin/nixible" "should create CLI executable" + + # test inventory content + jq -e '.test_group.hosts.test1.ansible_host' "${result.config.inventoryFile}" | grep -q "localhost" + assert_eq $? 0 "inventory should contain test host" + + jq -e '.test_group.vars.test_var' "${result.config.inventoryFile}" | grep -q "test_value" + assert_eq $? 0 "inventory should contain test variable" + + # test playbook content + assert_file_contains "${result.config.playbookFile}" "End-to-end test" "playbook should contain play name" + assert_file_contains "${result.config.playbookFile}" "test_group" "playbook should target test_group" + assert_file_contains "${result.config.playbookFile}" "Hello from" "playbook should contain debug message" + ''; + } + { + name = "SOPS example configuration"; + type = "script"; + script = let + # use the actual SOPS example from the repo + sopsConfig = ../examples/sops.nix; + result = nblib.mkNixible sopsConfig; + cli = nblib.mkNixibleCli sopsConfig; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + assert "-f ${result.config.inventoryFile}" "SOPS example should generate inventory" + assert "-f ${result.config.playbookFile}" "SOPS example should generate playbook" + assert "-x ${cli}/bin/nixible" "SOPS example should generate CLI" + + # test SOPS-specific content + assert_file_contains "${result.config.playbookFile}" "community.crypto.openssl_privatekey_pipe" "should use crypto collection" + assert_file_contains "${result.config.playbookFile}" "community.sops.sops_encrypt" "should use sops collection" + assert_file_contains "${result.config.playbookFile}" "no_log: true" "should have no_log for security" + ''; + } + ]; + }; +} diff --git a/tests/lib_test.nix b/tests/lib_test.nix new file mode 100644 index 0000000..3be6245 --- /dev/null +++ b/tests/lib_test.nix @@ -0,0 +1,182 @@ +{ + pkgs, + nblib, + ntlib, + ... +}: { + suites."Lib Tests" = { + pos = __curPos; + tests = [ + { + name = "mkNixibleCli generates executable"; + type = "script"; + script = let + config = { + playbook = [ + { + name = "Test CLI"; + hosts = "localhost"; + tasks = [ + { + debug.msg = "Testing CLI generation"; + } + ]; + } + ]; + }; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + # Check CLI contains expected content + assert_file_contains "${cli}/bin/nixible" "ansible-playbook" "should contain ansible-playbook command" + assert_file_contains "${cli}/bin/nixible" "ANSIBLE_COLLECTIONS_PATH" "should set collections path" + ''; + } + { + name = "inventory JSON generation"; + type = "script"; + script = let + config = { + inventory = { + webservers = { + hosts = { + web1 = {ansible_host = "192.168.1.10";}; + web2 = {ansible_host = "192.168.1.11";}; + }; + vars = { + http_port = 80; + }; + }; + }; + playbook = [ + { + name = "Test inventory"; + hosts = "webservers"; + tasks = []; + } + ]; + }; + result = nblib.mkNixible config; + inventoryFile = result.config.inventoryFile; + in + # sh + '' + ${ntlib.helpers.path [pkgs.jq pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + + # Check inventory file exists + assert "-f ${inventoryFile}" "inventory file should exist" + + # Check JSON structure + jq -e '.webservers.hosts.web1.ansible_host' "${inventoryFile}" | grep -q "192.168.1.10" + assert_eq $? 0 "should contain web1 host" + + jq -e '.webservers.vars.http_port' "${inventoryFile}" | grep -q "80" + assert_eq $? 0 "should contain http_port variable" + ''; + } + + { + name = "playbook YAML generation"; + type = "script"; + script = let + config = { + playbook = [ + { + name = "Test playbook generation"; + hosts = "localhost"; + become = true; + tasks = [ + { + name = "Install package"; + package = { + name = "nginx"; + state = "present"; + }; + } + { + name = "Start service"; + service = { + name = "nginx"; + state = "started"; + }; + } + ]; + } + ]; + }; + result = nblib.mkNixible config; + playbookFile = result.config.playbookFile; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + # Check playbook file exists + assert "-f ${playbookFile}" "playbook file should exist" + + # Check YAML structure + assert_file_contains "${playbookFile}" "Test playbook generation" "should contain play name" + assert_file_contains "${playbookFile}" "become: true" "should have become enabled" + assert_file_contains "${playbookFile}" "Install package" "should contain first task" + assert_file_contains "${playbookFile}" "nginx" "should contain nginx package" + ''; + } + { + name = "ansible package is configurable"; + type = "script"; + script = let + config = {pkgs, ...}: { + ansiblePackage = pkgs.python3Packages.ansible; + playbook = [ + { + name = "Test custom ansible"; + hosts = "localhost"; + tasks = []; + } + ]; + }; + cli = nblib.mkNixibleCli config; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + # check that custom ansible package is used + assert_file_contains "${cli}/bin/nixible" "${pkgs.python3Packages.ansible}" "should use custom ansible package" + ''; + } + { + name = "installed collections directory"; + type = "script"; + script = let + config = { + collections = { + "amazon-aws" = { + version = "10.1.0"; + hash = "sha256-w1wv0lYnuHXrpNubvePwKag4oM1k1I43HreFWYeIWgU="; + }; + "community-aws" = { + version = "10.0.0"; + hash = "sha256-oqsfmuztf8FLalwSDvRYcuvOVzLbWx/cEsYoUt8Dbn0="; + }; + }; + }; + result = nblib.mkNixible config; + collections = result.config.installedCollections; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + + assert "-d ${collections}" "collections directory should exist" + assert "-d ${collections}/ansible_collections/amazon/aws" "amazon/aws directory should exist" + assert "-d ${collections}/ansible_collections/community/aws" "community/aws directory should exist" + ''; + } + ]; + }; +}