chore: initial commit

This commit is contained in:
technofab 2025-07-15 19:28:42 +02:00
commit 7602719790
No known key found for this signature in database
24 changed files with 1916 additions and 0 deletions

2
.envrc Normal file
View file

@ -0,0 +1,2 @@
use flake . --impure --accept-flake-config

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.direnv
.devenv
result
.pre-commit-config.yaml

4
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,4 @@
include:
- component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@2.1.0
inputs:
version: 2.1.0

7
LICENSE.md Normal file
View file

@ -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.

65
README.md Normal file
View file

@ -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).

84
docs/examples.md Normal file
View file

@ -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";
};
}
];
}
```

BIN
docs/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

60
docs/index.md Normal file
View file

@ -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

215
docs/reference.md Normal file
View file

@ -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:<config-name>`. 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.

119
docs/usage.md Normal file
View file

@ -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/";
};
}];
}];
```

2
examples/.sops.yaml Normal file
View file

@ -0,0 +1,2 @@
keys: []
creation_rules: []

62
examples/sops.nix Normal file
View file

@ -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 = "";
}
];
}
];
}
];
}

376
flake.lock generated Normal file
View file

@ -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
}

215
flake.nix Normal file
View file

@ -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="
];
};
}

View file

@ -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}
''

91
lib/ansible-core.nix Normal file
View file

@ -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;
}

20
lib/default.nix Normal file
View file

@ -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;
}

6
lib/flake.nix Normal file
View file

@ -0,0 +1,6 @@
{
outputs = {...}: {
lib = import ./.;
flakeModule = ./flakeModule.nix;
};
}

33
lib/flakeModule.nix Normal file
View file

@ -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)
);
}
);
}

103
lib/module.nix Normal file
View file

@ -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" "$@"
'';
};
};
}

119
tests/cli_test.nix Normal file
View file

@ -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"
'';
}
];
};
}

101
tests/integration_test.nix Normal file
View file

@ -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"
'';
}
];
};
}

182
tests/lib_test.nix Normal file
View file

@ -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"
'';
}
];
};
}