config_ninja

Config Ninja 🥷

Ruff 🎨 poe (push) pylint codecov pre-commit.ci status Checked with mypy docs: pdoc readthedocs PyPI version Downloads

Similar to confd, manage your system configuration files by populating Jinja2 templates with data from a remote provider.

The config-ninja agent monitors the backend source for changes. When the source data is changed, the agent updates the local configuration file with the new data:

sequenceDiagram loop polling config-ninja->>backend: query for changes end backend->>+config-ninja: [backend changed] fetch config config-ninja->>-filesystem: write updated configuration file

Features

  • ✅ Integration with AWS AppConfig for managing server configuration files
  • ✅ Extensible design supports backends for new providers and formats
  • ✅ jinja2 templating for arbitrary configuration file formats
  • ✅ Execute poethepoet tasks after updating files

Installation

config-ninja is installed using the official installer or with pip / pipx. After installation, you can enable config-ninja as a systemd service.

Official Installer

The recommended way to install config-ninja is with the official installer:

curl -sSL https://config-ninja.github.io/config-ninja/install.py | python3 -

To view available installation options, run the installer with the --help flag:

curl -sSL https://config-ninja.github.io/config-ninja/install.py | python3 - --help
usage: install [-h] [--version VERSION] [--pre] [--uninstall] [--force] [--path PATH] [--backends BACKENDS]

Installs the latest (or given) version of config-ninja

options:
  -h, --help           show this help message and exit
  --version VERSION    install named version
  --pre                allow pre-release versions to be installed
  --uninstall          uninstall config-ninja
  --force              respond 'yes' to confirmation prompts; overwrite existing installations
  --path PATH          install config-ninja to this directory
  --backends BACKENDS  comma-separated list of package extras to install, or 'none' to install no backends

With pip / pipx

Alternatively, use pip / pipx to install all available backends (or choose a specific one):

pipx install 'config-ninja[all]'

With uv

uv tool install 'config-ninja[all]'

Enable the systemd Service

After installing config-ninja, enable it as a systemd service for the current user:

# omit '--user' to install the agent at the system level
config-ninja self install --user

Multiple instances of the service can also be installed to reference different settings files. For example, the following command will create the service named etc-config--ninja-alternate.service:

sudo config-ninja self install --config /etc/config-ninja/alternate.yaml

How It Works

To demonstrate how the mechanics work (using the local backend):

  1. create a settings file for config-ninja:

    cat <<EOF >config-ninja-settings.yaml
    CONFIG_NINJA_OBJECTS:
      example-0:
        dest:
          format: json
          path: ./.local/settings.json
      source:
        backend: local
        format: toml
        init:
          kwargs:
            path: ./.local/config.toml
    EOF
    
  2. run config-ninja in monitor mode:

    config-ninja apply --poll
    
  3. in a separate shell, create the config.toml:

    cat <<EOF >./.local/config.toml
    [example-0]
    a = "first value"
    b = "second value
    EOF
    
  4. Inspect the settings.json file created by config-ninja:

    cat ./.local/settings.json
    
    {
      "example-0": {
        "a": "first value",
        "b": "second value"
      }
    }
    
  5. Make changes to the data in config.toml, and config-ninja will update settings.json accordingly:

    cat <<EOF >>./.local/config.toml
    [example-1]
    c = "third value"
    d = "fourth value
    EOF
    cat ./.local/settings.json
    
    {
      "example-0": {
        "a": "first value",
        "b": "second value"
      },
      "example-1": {
        "c": "third value",
        "d": "fourth value"
      }
    }
    

Chances are, you'll want to update the config-ninja-settings.yaml file to use a remote backend (instead of local). See config_ninja.contrib for a list of supported config providers.

Navigation

config_ninja.cli

Commands and CLI documentation.

config_ninja.contrib

For supported backends.

config_ninja.contrib.appconfig

Integrate with the AWS AppConfig service.

config_ninja.contrib.local

Use a local file as the backend.

config_ninja.contrib.secretsmanager

Integrate with the AWS SecretsManager service.

config_ninja.hooks

Execute poethepoet tasks as callback hooks for backend updates.

config_ninja.settings

For settings and configuration.

config_ninja.systemd

Integration with systemd.

 1""".. include:: ../../README.md
 2
 3# Navigation
 4
 5## `config_ninja.cli`
 6
 7Commands and CLI documentation.
 8
 9## `config_ninja.contrib`
10
11For supported backends.
12
13### `config_ninja.contrib.appconfig`
14
15Integrate with the AWS AppConfig service.
16
17### `config_ninja.contrib.local`
18
19Use a local file as the backend.
20
21### `config_ninja.contrib.secretsmanager`
22
23Integrate with the AWS SecretsManager service.
24
25## `config_ninja.hooks`
26
27Execute [`poethepoet`](https://poethepoet.natn.io/) tasks as callback hooks for backend updates.
28
29## `config_ninja.settings`
30
31For settings and configuration.
32
33## `config_ninja.systemd`
34
35Integration with `systemd`.
36"""  # noqa: D415
37
38from __future__ import annotations
39
40import sys
41import warnings
42from pathlib import Path
43from typing import Any
44
45import pyspry
46
47__version__ = '0.0.0'
48
49from config_ninja.settings import DEFAULT_PATHS, load
50from config_ninja.settings import resolve_path as resolve_settings_path
51
52__all__ = ['DEFAULT_SETTINGS_PATHS', 'load_settings', 'resolve_settings_path']
53
54DEFAULT_SETTINGS_PATHS = DEFAULT_PATHS
55"""Check each of these locations for `config-ninja`_'s settings file.
56
57The following locations are checked (ordered by priority):
58
591. `./config-ninja-settings.yaml`
602. `~/config-ninja-settings.yaml`
613. `/etc/config-ninja/settings.yaml`
62
63.. _config-ninja: https://config-ninja.readthedocs.io/home.html
64"""
65
66
67def load_settings(path: Path) -> pyspry.Settings:  # pragma: no cover
68    """(deprecated) Load the settings file at the given path.
69
70    This function is deprecated and will be removed in a future release. Use `config_ninja.settings.load()` instead.
71    """
72    warnings.warn(
73        '`config_ninja.load_settings()` is deprecated and will be removed in a future release. Use '
74        '`config_ninja.settings.load()` instead.',
75        DeprecationWarning,
76        stacklevel=2,
77    )
78    return load(path).settings
79
80
81def main(*args: Any) -> None:  # pylint: disable=missing-function-docstring
82    """Entrypoint for the `config-ninja` CLI.
83
84    When arguments are provided, they are used to replace `sys.argv[1:]`.
85    """
86    if args:
87        sys.argv[1:] = list(args)
88
89    from config_ninja.cli import app
90
91    app(prog_name='config-ninja')
DEFAULT_SETTINGS_PATHS = [PosixPath('/home/runner/work/config-ninja/config-ninja/config-ninja-settings.yaml'), PosixPath('/home/runner/config-ninja-settings.yaml'), PosixPath('/etc/config-ninja/settings.yaml')]

Check each of these locations for config-ninja's settings file.

The following locations are checked (ordered by priority):

  1. ./config-ninja-settings.yaml
  2. ~/config-ninja-settings.yaml
  3. /etc/config-ninja/settings.yaml
def load_settings(path: pathlib.Path) -> pyspry.Settings:
68def load_settings(path: Path) -> pyspry.Settings:  # pragma: no cover
69    """(deprecated) Load the settings file at the given path.
70
71    This function is deprecated and will be removed in a future release. Use `config_ninja.settings.load()` instead.
72    """
73    warnings.warn(
74        '`config_ninja.load_settings()` is deprecated and will be removed in a future release. Use '
75        '`config_ninja.settings.load()` instead.',
76        DeprecationWarning,
77        stacklevel=2,
78    )
79    return load(path).settings

(deprecated) Load the settings file at the given path.

This function is deprecated and will be removed in a future release. Use config_ninja.settings.load() instead.

def resolve_settings_path() -> pathlib.Path:
137def resolve_path() -> Path:
138    """Return the first path in `DEFAULT_PATHS` that exists."""
139    for path in DEFAULT_PATHS:
140        if path.is_file():
141            return path
142
143    raise FileNotFoundError('Could not find config-ninja settings', DEFAULT_PATHS)

Return the first path in DEFAULT_PATHS that exists.