clixz 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clixz-1.2.0/PKG-INFO +259 -0
- clixz-1.2.0/README.md +237 -0
- clixz-1.2.0/pyproject.toml +66 -0
- clixz-1.2.0/setup.cfg +4 -0
- clixz-1.2.0/src/clixz/__init__.py +3 -0
- clixz-1.2.0/src/clixz/__main__.py +4 -0
- clixz-1.2.0/src/clixz/admind.py +445 -0
- clixz-1.2.0/src/clixz/archive.py +138 -0
- clixz-1.2.0/src/clixz/cli.py +1752 -0
- clixz-1.2.0/src/clixz/compat.py +56 -0
- clixz-1.2.0/src/clixz/config.py +482 -0
- clixz-1.2.0/src/clixz/default_config.yaml +127 -0
- clixz-1.2.0/src/clixz/dev.py +204 -0
- clixz-1.2.0/src/clixz/image.py +64 -0
- clixz-1.2.0/src/clixz/meta.py +301 -0
- clixz-1.2.0/src/clixz/policy.py +733 -0
- clixz-1.2.0/src/clixz/runnerd.py +224 -0
- clixz-1.2.0/src/clixz/scaffold.py +90 -0
- clixz-1.2.0/src/clixz/spec.py +423 -0
- clixz-1.2.0/src/clixz/system.py +286 -0
- clixz-1.2.0/src/clixz/update.py +214 -0
- clixz-1.2.0/src/clixz.egg-info/PKG-INFO +259 -0
- clixz-1.2.0/src/clixz.egg-info/SOURCES.txt +38 -0
- clixz-1.2.0/src/clixz.egg-info/dependency_links.txt +1 -0
- clixz-1.2.0/src/clixz.egg-info/entry_points.txt +4 -0
- clixz-1.2.0/src/clixz.egg-info/requires.txt +6 -0
- clixz-1.2.0/src/clixz.egg-info/top_level.txt +1 -0
- clixz-1.2.0/tests/test_acl.py +414 -0
- clixz-1.2.0/tests/test_admind.py +224 -0
- clixz-1.2.0/tests/test_apply_flow.py +125 -0
- clixz-1.2.0/tests/test_archive.py +129 -0
- clixz-1.2.0/tests/test_compat.py +73 -0
- clixz-1.2.0/tests/test_config.py +86 -0
- clixz-1.2.0/tests/test_dev.py +168 -0
- clixz-1.2.0/tests/test_image.py +215 -0
- clixz-1.2.0/tests/test_meta.py +168 -0
- clixz-1.2.0/tests/test_runnerd.py +101 -0
- clixz-1.2.0/tests/test_scaffold.py +77 -0
- clixz-1.2.0/tests/test_spec.py +243 -0
- clixz-1.2.0/tests/test_update.py +193 -0
clixz-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clixz
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: CLI to manage Docker services under /srv/docker (ownership, permissions, ACLs)
|
|
5
|
+
Author: clixz
|
|
6
|
+
Project-URL: Homepage, https://github.com/Coxyz/Clixz
|
|
7
|
+
Project-URL: Repository, https://github.com/Coxyz/Clixz
|
|
8
|
+
Project-URL: Issues, https://github.com/Coxyz/Clixz/issues
|
|
9
|
+
Keywords: docker,acl,permissions,cli,sysadmin
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Requires-Dist: rich>=13.7
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Provides-Extra: release
|
|
21
|
+
Requires-Dist: bump-my-version>=0.20; extra == "release"
|
|
22
|
+
|
|
23
|
+
# clixz
|
|
24
|
+
|
|
25
|
+
CLI to manage Docker services under `/srv/docker` following clixz rules
|
|
26
|
+
(ownership, permissions, POSIX ACLs).
|
|
27
|
+
|
|
28
|
+
Replaces `check_fix_permission.zsh` + `services.zsh` with a single typed Python
|
|
29
|
+
tool driven by a YAML configuration.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
clixz is published on PyPI as the [`clixz`](https://pypi.org/project/clixz/)
|
|
34
|
+
package — the installed command stays `clixz`. It needs root for most operations
|
|
35
|
+
(`chown` / `setfacl`), so install it **system-wide**. Commands that need root
|
|
36
|
+
re-exec themselves through `sudo` automatically — you no longer have to prefix
|
|
37
|
+
them yourself (set `CLIXZ_NO_SUDO=1` to opt out, e.g. in containers running as
|
|
38
|
+
root).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sudo apt install -y pipx
|
|
42
|
+
|
|
43
|
+
# Install into an isolated venv under /opt, with the binary on the system PATH.
|
|
44
|
+
sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install clixz
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> With pipx ≥ 1.5 you can use the shorter `sudo pipx install --global clixz`
|
|
48
|
+
> instead. Debian 12 ships pipx 1.4.3, which needs the `env` form above.
|
|
49
|
+
|
|
50
|
+
Then run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
clixz check
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Optionally enable shell completion for your user (no sudo):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
clixz --install-completion
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Once installed, completion suggests service names for commands that take a
|
|
63
|
+
service argument (`check`, `apply`, `dev add/remove`, `meta scaffold/validate`),
|
|
64
|
+
categories for `-C/--category`, and existing images for `image remove`.
|
|
65
|
+
|
|
66
|
+
### Update
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx upgrade clixz
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Migrating from a manual install
|
|
73
|
+
|
|
74
|
+
Earlier setups used hand-written `clixz` / `clixz-update` wrapper scripts and a
|
|
75
|
+
venv in `/usr/local/libexec/clixz`. Remove them before installing from PyPI:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
sudo rm -f /usr/local/bin/clixz /usr/local/bin/clixz-update
|
|
79
|
+
sudo rm -rf /usr/local/libexec/clixz
|
|
80
|
+
rm -f ~/.zsh/completions/_clixz ~/.zsh/completions/_coxyz ~/.zcompdump* # stale completion artefacts (incl. pre-rename)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
(`/etc/clixz/config.yaml` is kept — it is your configuration, not part of the
|
|
84
|
+
install.)
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
`clixz` reads, in order: `--config FILE`, `/etc/clixz/config.yaml`,
|
|
89
|
+
`~/.config/clixz/config.yaml`, then the bundled defaults.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
clixz show-config # inspect the resolved config
|
|
93
|
+
clixz edit # create/edit /etc/clixz/config.yaml (seeded from defaults)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Example excludes in `config.yaml`:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
exclude:
|
|
100
|
+
- "*.bak"
|
|
101
|
+
- "*/do_not_touch/"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Commands
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
clixz list # list services with image, ports, status
|
|
108
|
+
clixz list -C apps # filter by category
|
|
109
|
+
|
|
110
|
+
clixz check # validate config + audit all services (exit 1 on drift)
|
|
111
|
+
clixz check bitwarden # audit one service
|
|
112
|
+
clixz check apps/bitwarden -v # verbose (show OK findings too)
|
|
113
|
+
|
|
114
|
+
clixz apply # preview planned fixes, confirm, then apply
|
|
115
|
+
clixz apply bitwarden -y
|
|
116
|
+
|
|
117
|
+
clixz create # interactive prompts, confirm, then create
|
|
118
|
+
clixz create -C apps -n myapp -y
|
|
119
|
+
|
|
120
|
+
clixz manifest # aggregate every service.yaml → API manifest
|
|
121
|
+
clixz manifest --dry-run # validate + preview without writing
|
|
122
|
+
clixz meta scaffold apps/nginx # add a service.yaml template to an existing service
|
|
123
|
+
clixz meta validate # validate all service.yaml descriptors
|
|
124
|
+
|
|
125
|
+
clixz dev add apps/nginx # make a service editable via code-server
|
|
126
|
+
clixz dev remove apps/nginx # revoke it
|
|
127
|
+
clixz dev list # show dev-enabled services
|
|
128
|
+
|
|
129
|
+
clixz image add api # scaffold a self-built image context in /opt/images
|
|
130
|
+
clixz image remove api # delete an image build context
|
|
131
|
+
clixz image list # list image build contexts
|
|
132
|
+
|
|
133
|
+
clixz show-config # print resolved config
|
|
134
|
+
clixz edit # edit /etc/clixz/config.yaml
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Most operations require root (`chown` / `setfacl`). clixz elevates itself with
|
|
138
|
+
`sudo` automatically when needed, so the examples above work without a prefix
|
|
139
|
+
(use `CLIXZ_NO_SUDO=1` to disable auto-elevation).
|
|
140
|
+
|
|
141
|
+
## How it works
|
|
142
|
+
|
|
143
|
+
- **Config** (`/etc/clixz/config.yaml` or bundled default) defines:
|
|
144
|
+
- root dir, ACL principals, authorized categories
|
|
145
|
+
- `exclude` glob patterns to ignore paths during audit/apply
|
|
146
|
+
- per-path rules: mode, ACL perms, optional owner override, audit-only flag
|
|
147
|
+
- **`check`**: read-only. First validates the config's *structure* (missing keys,
|
|
148
|
+
bad values, sections nested in the wrong place), then audits permissions/ACL —
|
|
149
|
+
reporting drift and warn-only (`data/`, `.env`).
|
|
150
|
+
- **`apply`**: shows planned changes, asks for confirmation, then applies fixes.
|
|
151
|
+
- Touches: category/service dirs, `compose.yaml`, the `config/` directory.
|
|
152
|
+
- Never touches: `data/` contents, `.env` files (audit-only).
|
|
153
|
+
- Creates required missing directories before applying path fixes.
|
|
154
|
+
- **Dev-mode awareness**: both `check` and `apply` first read the code-server
|
|
155
|
+
compose to learn which services are dev-enabled. For those services the dev
|
|
156
|
+
principal's recursive ACL **and** the default ACL on `config/` and `data/` are
|
|
157
|
+
treated as *expected* — not drift — and any fix is non-destructive (it never
|
|
158
|
+
uses `setfacl --set`/`-b`, which would wipe the dev grant). A leftover dev ACL
|
|
159
|
+
on a service that is *not* dev-enabled is still correctly flagged for removal.
|
|
160
|
+
- **`create`**: scaffolds `<category>/<service>/{config/,data/}` plus **empty**
|
|
161
|
+
`compose.yaml` and `.env`, a `service.yaml` template, with correct owners +
|
|
162
|
+
perms + ACL. It does not template `compose.yaml` — you fill it in. Then it
|
|
163
|
+
refreshes the dashboard manifest.
|
|
164
|
+
- **`service.yaml`** (dashboard descriptor): a per-service file describing how
|
|
165
|
+
the service appears on the clixz dashboard — `name`, `icon`, `description`,
|
|
166
|
+
`public` (true ⇒ exposed by the API, false ⇒ hidden entirely), optional
|
|
167
|
+
`url`/`kind`/`container`/`tags`, and a `details:` block (summary, features,
|
|
168
|
+
internal `ports`, `depends_on`, `tech`). Put only **non-sensitive** info here.
|
|
169
|
+
Its permissions are governed by the `service_file` rule (default `640`).
|
|
170
|
+
- **`manifest`**: reads every `service.yaml`, validates it, and aggregates the
|
|
171
|
+
**public** ones into the JSON file at `api.manifest`
|
|
172
|
+
(default `/srv/docker/apps/api/data/manifest.json`, mode `644`), which the
|
|
173
|
+
clixz-api container mounts read-only and serves at `/api/services`. Private
|
|
174
|
+
descriptors never reach the manifest.
|
|
175
|
+
- **`meta scaffold <service>`**: drops a `service.yaml` template into an existing
|
|
176
|
+
service (won't overwrite). **`meta validate`**: validates descriptors only.
|
|
177
|
+
- **`check`** also validates every `service.yaml` (a missing one is a warning; a
|
|
178
|
+
malformed one is an error that fails the check).
|
|
179
|
+
- **Self-built images** live **outside** the service tree, in their own build
|
|
180
|
+
context under `images.dir` (default `/opt/images/<name>/` — Dockerfile +
|
|
181
|
+
sources). The matching service under `/srv/docker` stays empty: it just
|
|
182
|
+
*consumes* the built image, exactly like a third-party image. Same convention
|
|
183
|
+
for source repos under `repos.dir` (default `/opt/repos`).
|
|
184
|
+
- **`image add/remove/list`**: `add` scaffolds `<images.dir>/<name>/` with the
|
|
185
|
+
configured `owner`/`mode` (default `boxyz_dev:boxyz_dev`, `775`) plus a
|
|
186
|
+
Dockerfile template; `remove` deletes the whole context; `list` shows each
|
|
187
|
+
context with its Dockerfile/compliance.
|
|
188
|
+
- The `775` mode means the dev principal's group can edit while *others* (the
|
|
189
|
+
root Komodo Periphery process) can read — so Komodo builds the context with
|
|
190
|
+
no per-service ACL and **without** touching `/srv/docker` isolation.
|
|
191
|
+
- Configure the locations under the `images:` and `repos:` sections; `check`/
|
|
192
|
+
`apply` then enforce the owner/mode of every `<dir>/<name>` directory.
|
|
193
|
+
- Optional `acl:` on either section adds named ACL entries on every
|
|
194
|
+
`<dir>/<name>` directory (same `{ principal: perms }` form as a rule), for
|
|
195
|
+
when owner/mode alone isn't enough:
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
repos:
|
|
199
|
+
dir: /opt/repos
|
|
200
|
+
owner: "boxyz_dev:boxyz_dev"
|
|
201
|
+
mode: "775"
|
|
202
|
+
acl:
|
|
203
|
+
komodo: "rx" # grant the komodo principal read+exec via ACL
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Build with Komodo (or the CLI): context = the image's own directory.
|
|
208
|
+
docker build -t api-clixz:latest /opt/images/api
|
|
209
|
+
```
|
|
210
|
+
- **`list`**: parses each `compose.yaml` for image/ports and runs an audit
|
|
211
|
+
to show a compliance status.
|
|
212
|
+
- **`dev add/remove/list`**: makes a service editable through code-server.
|
|
213
|
+
`add` grants the `dev.principal` group (default `boxyz_dev`) a recursive
|
|
214
|
+
read/write ACL on the service's `config/` and `data/` (existing files **and**
|
|
215
|
+
a default ACL so new files inherit it), and mounts both dirs into the
|
|
216
|
+
code-server compose under `/workspace/services/<category>/<service>/`. `remove`
|
|
217
|
+
revokes *only* that group's ACL entry and unmounts. The managed mounts live in
|
|
218
|
+
a marker-delimited block (`# >>> clixz dev ... >>>`) that is the single source
|
|
219
|
+
of truth — `list` reads it; everything else in the compose is left untouched.
|
|
220
|
+
Configured under the `dev:` key in `config.yaml`.
|
|
221
|
+
|
|
222
|
+
### ACL handling
|
|
223
|
+
|
|
224
|
+
A path governed by an ACL rule is brought to compliance with a **single
|
|
225
|
+
`setfacl --set` call** that writes the base entries (`u::`/`g::`/`o::`, i.e. the
|
|
226
|
+
octal mode) and the named entries together. `setfacl` then recomputes the ACL
|
|
227
|
+
*mask* as the union of the owning group and every named entry, so each entry
|
|
228
|
+
stays fully effective — `getfacl` never shows an `#effective:` restriction.
|
|
229
|
+
|
|
230
|
+
`clixz` deliberately never runs `chmod` on an ACL-managed path: a `chmod` after
|
|
231
|
+
a `setfacl` would rewrite the mask instead of the group bits and silently shrink
|
|
232
|
+
the effective rights of every named entry.
|
|
233
|
+
|
|
234
|
+
One consequence: when a named entry grants more than the owning group (e.g. a
|
|
235
|
+
principal with `rw` on a `750` directory), the mask widens and `ls -l` shows the
|
|
236
|
+
wider group digit (`770`). That is correct POSIX behaviour — the audit compares
|
|
237
|
+
ACL entries, not the displayed mode.
|
|
238
|
+
|
|
239
|
+
## File layout (enforced)
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
/srv/docker/<category>/<service>/
|
|
243
|
+
├── compose.yaml 660 svc_<cat>:svc_<cat> + ACL principals
|
|
244
|
+
├── config/ 750 svc_<cat>:svc_<cat> + ACL principals
|
|
245
|
+
│ └── ... (contents not audited)
|
|
246
|
+
└── data/ 750 svc_<cat>:svc_<cat> no ACL (audit only)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
make test # run the test suite
|
|
253
|
+
make build # build sdist + wheel into dist/
|
|
254
|
+
make release # tag the current version and push (CI publishes to PyPI)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Releasing: bump `__version__` in `src/clixz/__init__.py`, commit, then
|
|
258
|
+
`make release`. The tag `vX.Y.Z` triggers `.github/workflows/publish.yml`,
|
|
259
|
+
which publishes to PyPI via Trusted Publishing.
|
clixz-1.2.0/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# clixz
|
|
2
|
+
|
|
3
|
+
CLI to manage Docker services under `/srv/docker` following clixz rules
|
|
4
|
+
(ownership, permissions, POSIX ACLs).
|
|
5
|
+
|
|
6
|
+
Replaces `check_fix_permission.zsh` + `services.zsh` with a single typed Python
|
|
7
|
+
tool driven by a YAML configuration.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
clixz is published on PyPI as the [`clixz`](https://pypi.org/project/clixz/)
|
|
12
|
+
package — the installed command stays `clixz`. It needs root for most operations
|
|
13
|
+
(`chown` / `setfacl`), so install it **system-wide**. Commands that need root
|
|
14
|
+
re-exec themselves through `sudo` automatically — you no longer have to prefix
|
|
15
|
+
them yourself (set `CLIXZ_NO_SUDO=1` to opt out, e.g. in containers running as
|
|
16
|
+
root).
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
sudo apt install -y pipx
|
|
20
|
+
|
|
21
|
+
# Install into an isolated venv under /opt, with the binary on the system PATH.
|
|
22
|
+
sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install clixz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> With pipx ≥ 1.5 you can use the shorter `sudo pipx install --global clixz`
|
|
26
|
+
> instead. Debian 12 ships pipx 1.4.3, which needs the `env` form above.
|
|
27
|
+
|
|
28
|
+
Then run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
clixz check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Optionally enable shell completion for your user (no sudo):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
clixz --install-completion
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Once installed, completion suggests service names for commands that take a
|
|
41
|
+
service argument (`check`, `apply`, `dev add/remove`, `meta scaffold/validate`),
|
|
42
|
+
categories for `-C/--category`, and existing images for `image remove`.
|
|
43
|
+
|
|
44
|
+
### Update
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx upgrade clixz
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Migrating from a manual install
|
|
51
|
+
|
|
52
|
+
Earlier setups used hand-written `clixz` / `clixz-update` wrapper scripts and a
|
|
53
|
+
venv in `/usr/local/libexec/clixz`. Remove them before installing from PyPI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sudo rm -f /usr/local/bin/clixz /usr/local/bin/clixz-update
|
|
57
|
+
sudo rm -rf /usr/local/libexec/clixz
|
|
58
|
+
rm -f ~/.zsh/completions/_clixz ~/.zsh/completions/_coxyz ~/.zcompdump* # stale completion artefacts (incl. pre-rename)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(`/etc/clixz/config.yaml` is kept — it is your configuration, not part of the
|
|
62
|
+
install.)
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
`clixz` reads, in order: `--config FILE`, `/etc/clixz/config.yaml`,
|
|
67
|
+
`~/.config/clixz/config.yaml`, then the bundled defaults.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
clixz show-config # inspect the resolved config
|
|
71
|
+
clixz edit # create/edit /etc/clixz/config.yaml (seeded from defaults)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example excludes in `config.yaml`:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
exclude:
|
|
78
|
+
- "*.bak"
|
|
79
|
+
- "*/do_not_touch/"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
clixz list # list services with image, ports, status
|
|
86
|
+
clixz list -C apps # filter by category
|
|
87
|
+
|
|
88
|
+
clixz check # validate config + audit all services (exit 1 on drift)
|
|
89
|
+
clixz check bitwarden # audit one service
|
|
90
|
+
clixz check apps/bitwarden -v # verbose (show OK findings too)
|
|
91
|
+
|
|
92
|
+
clixz apply # preview planned fixes, confirm, then apply
|
|
93
|
+
clixz apply bitwarden -y
|
|
94
|
+
|
|
95
|
+
clixz create # interactive prompts, confirm, then create
|
|
96
|
+
clixz create -C apps -n myapp -y
|
|
97
|
+
|
|
98
|
+
clixz manifest # aggregate every service.yaml → API manifest
|
|
99
|
+
clixz manifest --dry-run # validate + preview without writing
|
|
100
|
+
clixz meta scaffold apps/nginx # add a service.yaml template to an existing service
|
|
101
|
+
clixz meta validate # validate all service.yaml descriptors
|
|
102
|
+
|
|
103
|
+
clixz dev add apps/nginx # make a service editable via code-server
|
|
104
|
+
clixz dev remove apps/nginx # revoke it
|
|
105
|
+
clixz dev list # show dev-enabled services
|
|
106
|
+
|
|
107
|
+
clixz image add api # scaffold a self-built image context in /opt/images
|
|
108
|
+
clixz image remove api # delete an image build context
|
|
109
|
+
clixz image list # list image build contexts
|
|
110
|
+
|
|
111
|
+
clixz show-config # print resolved config
|
|
112
|
+
clixz edit # edit /etc/clixz/config.yaml
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Most operations require root (`chown` / `setfacl`). clixz elevates itself with
|
|
116
|
+
`sudo` automatically when needed, so the examples above work without a prefix
|
|
117
|
+
(use `CLIXZ_NO_SUDO=1` to disable auto-elevation).
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
- **Config** (`/etc/clixz/config.yaml` or bundled default) defines:
|
|
122
|
+
- root dir, ACL principals, authorized categories
|
|
123
|
+
- `exclude` glob patterns to ignore paths during audit/apply
|
|
124
|
+
- per-path rules: mode, ACL perms, optional owner override, audit-only flag
|
|
125
|
+
- **`check`**: read-only. First validates the config's *structure* (missing keys,
|
|
126
|
+
bad values, sections nested in the wrong place), then audits permissions/ACL —
|
|
127
|
+
reporting drift and warn-only (`data/`, `.env`).
|
|
128
|
+
- **`apply`**: shows planned changes, asks for confirmation, then applies fixes.
|
|
129
|
+
- Touches: category/service dirs, `compose.yaml`, the `config/` directory.
|
|
130
|
+
- Never touches: `data/` contents, `.env` files (audit-only).
|
|
131
|
+
- Creates required missing directories before applying path fixes.
|
|
132
|
+
- **Dev-mode awareness**: both `check` and `apply` first read the code-server
|
|
133
|
+
compose to learn which services are dev-enabled. For those services the dev
|
|
134
|
+
principal's recursive ACL **and** the default ACL on `config/` and `data/` are
|
|
135
|
+
treated as *expected* — not drift — and any fix is non-destructive (it never
|
|
136
|
+
uses `setfacl --set`/`-b`, which would wipe the dev grant). A leftover dev ACL
|
|
137
|
+
on a service that is *not* dev-enabled is still correctly flagged for removal.
|
|
138
|
+
- **`create`**: scaffolds `<category>/<service>/{config/,data/}` plus **empty**
|
|
139
|
+
`compose.yaml` and `.env`, a `service.yaml` template, with correct owners +
|
|
140
|
+
perms + ACL. It does not template `compose.yaml` — you fill it in. Then it
|
|
141
|
+
refreshes the dashboard manifest.
|
|
142
|
+
- **`service.yaml`** (dashboard descriptor): a per-service file describing how
|
|
143
|
+
the service appears on the clixz dashboard — `name`, `icon`, `description`,
|
|
144
|
+
`public` (true ⇒ exposed by the API, false ⇒ hidden entirely), optional
|
|
145
|
+
`url`/`kind`/`container`/`tags`, and a `details:` block (summary, features,
|
|
146
|
+
internal `ports`, `depends_on`, `tech`). Put only **non-sensitive** info here.
|
|
147
|
+
Its permissions are governed by the `service_file` rule (default `640`).
|
|
148
|
+
- **`manifest`**: reads every `service.yaml`, validates it, and aggregates the
|
|
149
|
+
**public** ones into the JSON file at `api.manifest`
|
|
150
|
+
(default `/srv/docker/apps/api/data/manifest.json`, mode `644`), which the
|
|
151
|
+
clixz-api container mounts read-only and serves at `/api/services`. Private
|
|
152
|
+
descriptors never reach the manifest.
|
|
153
|
+
- **`meta scaffold <service>`**: drops a `service.yaml` template into an existing
|
|
154
|
+
service (won't overwrite). **`meta validate`**: validates descriptors only.
|
|
155
|
+
- **`check`** also validates every `service.yaml` (a missing one is a warning; a
|
|
156
|
+
malformed one is an error that fails the check).
|
|
157
|
+
- **Self-built images** live **outside** the service tree, in their own build
|
|
158
|
+
context under `images.dir` (default `/opt/images/<name>/` — Dockerfile +
|
|
159
|
+
sources). The matching service under `/srv/docker` stays empty: it just
|
|
160
|
+
*consumes* the built image, exactly like a third-party image. Same convention
|
|
161
|
+
for source repos under `repos.dir` (default `/opt/repos`).
|
|
162
|
+
- **`image add/remove/list`**: `add` scaffolds `<images.dir>/<name>/` with the
|
|
163
|
+
configured `owner`/`mode` (default `boxyz_dev:boxyz_dev`, `775`) plus a
|
|
164
|
+
Dockerfile template; `remove` deletes the whole context; `list` shows each
|
|
165
|
+
context with its Dockerfile/compliance.
|
|
166
|
+
- The `775` mode means the dev principal's group can edit while *others* (the
|
|
167
|
+
root Komodo Periphery process) can read — so Komodo builds the context with
|
|
168
|
+
no per-service ACL and **without** touching `/srv/docker` isolation.
|
|
169
|
+
- Configure the locations under the `images:` and `repos:` sections; `check`/
|
|
170
|
+
`apply` then enforce the owner/mode of every `<dir>/<name>` directory.
|
|
171
|
+
- Optional `acl:` on either section adds named ACL entries on every
|
|
172
|
+
`<dir>/<name>` directory (same `{ principal: perms }` form as a rule), for
|
|
173
|
+
when owner/mode alone isn't enough:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
repos:
|
|
177
|
+
dir: /opt/repos
|
|
178
|
+
owner: "boxyz_dev:boxyz_dev"
|
|
179
|
+
mode: "775"
|
|
180
|
+
acl:
|
|
181
|
+
komodo: "rx" # grant the komodo principal read+exec via ACL
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Build with Komodo (or the CLI): context = the image's own directory.
|
|
186
|
+
docker build -t api-clixz:latest /opt/images/api
|
|
187
|
+
```
|
|
188
|
+
- **`list`**: parses each `compose.yaml` for image/ports and runs an audit
|
|
189
|
+
to show a compliance status.
|
|
190
|
+
- **`dev add/remove/list`**: makes a service editable through code-server.
|
|
191
|
+
`add` grants the `dev.principal` group (default `boxyz_dev`) a recursive
|
|
192
|
+
read/write ACL on the service's `config/` and `data/` (existing files **and**
|
|
193
|
+
a default ACL so new files inherit it), and mounts both dirs into the
|
|
194
|
+
code-server compose under `/workspace/services/<category>/<service>/`. `remove`
|
|
195
|
+
revokes *only* that group's ACL entry and unmounts. The managed mounts live in
|
|
196
|
+
a marker-delimited block (`# >>> clixz dev ... >>>`) that is the single source
|
|
197
|
+
of truth — `list` reads it; everything else in the compose is left untouched.
|
|
198
|
+
Configured under the `dev:` key in `config.yaml`.
|
|
199
|
+
|
|
200
|
+
### ACL handling
|
|
201
|
+
|
|
202
|
+
A path governed by an ACL rule is brought to compliance with a **single
|
|
203
|
+
`setfacl --set` call** that writes the base entries (`u::`/`g::`/`o::`, i.e. the
|
|
204
|
+
octal mode) and the named entries together. `setfacl` then recomputes the ACL
|
|
205
|
+
*mask* as the union of the owning group and every named entry, so each entry
|
|
206
|
+
stays fully effective — `getfacl` never shows an `#effective:` restriction.
|
|
207
|
+
|
|
208
|
+
`clixz` deliberately never runs `chmod` on an ACL-managed path: a `chmod` after
|
|
209
|
+
a `setfacl` would rewrite the mask instead of the group bits and silently shrink
|
|
210
|
+
the effective rights of every named entry.
|
|
211
|
+
|
|
212
|
+
One consequence: when a named entry grants more than the owning group (e.g. a
|
|
213
|
+
principal with `rw` on a `750` directory), the mask widens and `ls -l` shows the
|
|
214
|
+
wider group digit (`770`). That is correct POSIX behaviour — the audit compares
|
|
215
|
+
ACL entries, not the displayed mode.
|
|
216
|
+
|
|
217
|
+
## File layout (enforced)
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
/srv/docker/<category>/<service>/
|
|
221
|
+
├── compose.yaml 660 svc_<cat>:svc_<cat> + ACL principals
|
|
222
|
+
├── config/ 750 svc_<cat>:svc_<cat> + ACL principals
|
|
223
|
+
│ └── ... (contents not audited)
|
|
224
|
+
└── data/ 750 svc_<cat>:svc_<cat> no ACL (audit only)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
make test # run the test suite
|
|
231
|
+
make build # build sdist + wheel into dist/
|
|
232
|
+
make release # tag the current version and push (CI publishes to PyPI)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Releasing: bump `__version__` in `src/clixz/__init__.py`, commit, then
|
|
236
|
+
`make release`. The tag `vX.Y.Z` triggers `.github/workflows/publish.yml`,
|
|
237
|
+
which publishes to PyPI via Trusted Publishing.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "clixz"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "CLI to manage Docker services under /srv/docker (ownership, permissions, ACLs)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "clixz" }]
|
|
12
|
+
keywords = ["docker", "acl", "permissions", "cli", "sysadmin"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: System Administrators",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Topic :: System :: Systems Administration",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"typer>=0.12",
|
|
22
|
+
"rich>=13.7",
|
|
23
|
+
"pyyaml>=6.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
release = ["bump-my-version>=0.20"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/Coxyz/Clixz"
|
|
31
|
+
Repository = "https://github.com/Coxyz/Clixz"
|
|
32
|
+
Issues = "https://github.com/Coxyz/Clixz/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
clixz = "clixz.cli:app"
|
|
36
|
+
# Privileged daemon, started by systemd as root. Installing it as a console
|
|
37
|
+
# script places its code under the root-owned venv, so the ExecStart target
|
|
38
|
+
# cannot be rewritten by an application group.
|
|
39
|
+
clixz-admind = "clixz.admind:main"
|
|
40
|
+
# Unprivileged gateway for the MCP container; same reasoning on the install path.
|
|
41
|
+
clixz-runnerd = "clixz.runnerd:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.dynamic]
|
|
44
|
+
version = { attr = "clixz.__version__" }
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
clixz = ["default_config.yaml"]
|
|
51
|
+
|
|
52
|
+
# `make release` drives this: bumps the version, commits, and creates a vX.Y.Z
|
|
53
|
+
# tag. Pushing that tag triggers .github/workflows/publish.yml → PyPI.
|
|
54
|
+
[tool.bumpversion]
|
|
55
|
+
current_version = "1.2.0"
|
|
56
|
+
allow_dirty = false
|
|
57
|
+
commit = true
|
|
58
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
59
|
+
tag = true
|
|
60
|
+
tag_name = "v{new_version}"
|
|
61
|
+
tag_message = "Release v{new_version}"
|
|
62
|
+
|
|
63
|
+
[[tool.bumpversion.files]]
|
|
64
|
+
filename = "src/clixz/__init__.py"
|
|
65
|
+
search = '__version__ = "{current_version}"'
|
|
66
|
+
replace = '__version__ = "{new_version}"'
|
clixz-1.2.0/setup.cfg
ADDED