feat(tooling): containerize dns-cli in tooling-cli/dns
Consolidate the Technitium DNS CLI into tooling-cli/dns/ with a
containerized bash CLI (src/dns_cli.sh), Dockerfile (alpine +
curl/python3/bind-tools/bash), README, AGENTS.md, validate.sh, and
.env.example. Env vars aligned to the centralized ~/.creds/technitium.env
store (TECHNITIUM_URL, TECHNITIUM_DNS_TOKEN, TECHNITIUM_DNS_ZONE).
Image built and pushed to the registry as
git.knownelement.com/reachableceo/dns-cli:latest. Verified live:
zones, list, and search all return data.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
# Technitium DNS connection details
|
||||||
|
# Copy this file to .env and fill in real values. The .env file is gitignored.
|
||||||
|
TECHNITIUM_URL=http://pfv-netinfra-01:5380
|
||||||
|
TECHNITIUM_DNS_TOKEN=your-dns-api-token-here
|
||||||
|
TECHNITIUM_DNS_ZONE=knel.net
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Secrets - never commit
|
||||||
|
.env
|
||||||
|
|
||||||
|
# OS / editor cruft
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# AGENTS.md — DNS CLI tooling
|
||||||
|
|
||||||
|
This directory holds the `dns-cli` Docker container — a bash CLI over the
|
||||||
|
Technitium DNS Server REST API. Every Crush session that works on or with
|
||||||
|
this tool should read this file first.
|
||||||
|
|
||||||
|
## Connection & invocation
|
||||||
|
|
||||||
|
Invoke the real container with `docker run`. Credentials come from the
|
||||||
|
centralized credential store (see `tooling-cli/KNELCredsManager`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
There is intentionally **no `bin/` wrapper** — invoke the real container, as
|
||||||
|
the project-wide rules require. For readability in an interactive session you
|
||||||
|
may define a throwaway shell alias (do not commit one):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alias dns='docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest'
|
||||||
|
```
|
||||||
|
|
||||||
|
The examples below write `dns <cmd>` for brevity; expand it to the full
|
||||||
|
`docker run` line (or set the alias) before running.
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Instance | `http://pfv-netinfra-01.knel.net:5380` |
|
||||||
|
| Default zone | `knel.net` |
|
||||||
|
| Credentials | `~/.creds/technitium.env` (`TECHNITIUM_URL`, `TECHNITIUM_DNS_TOKEN`, `TECHNITIUM_DNS_ZONE`) |
|
||||||
|
| CLI image | `git.knownelement.com/reachableceo/dns-cli:latest` |
|
||||||
|
| Source | `src/dns_cli.sh` in this directory |
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all zones:
|
||||||
|
dns zones
|
||||||
|
|
||||||
|
# List records in the default zone:
|
||||||
|
dns list
|
||||||
|
|
||||||
|
# Search for a record:
|
||||||
|
dns search ns1
|
||||||
|
|
||||||
|
# Resolve a name:
|
||||||
|
dns get pfv-netinfra-01
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command reference
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `zones` | List all zones. |
|
||||||
|
| `list [zone]` | List records in a zone (default: `TECHNITIUM_DNS_ZONE`). |
|
||||||
|
| `get <name>` | Resolve a record via `dig` (A/AAAA). |
|
||||||
|
| `add <name> <ip> [ttl]` | Add an A record with auto-PTR (default TTL 3600). |
|
||||||
|
| `delete <name> <ip>` | Delete an A record. |
|
||||||
|
| `flush` | Flush the DNS cache. |
|
||||||
|
| `search <pattern>` | Search records by name pattern (case-insensitive). |
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
### Pattern: add a new DNS record
|
||||||
|
1. `dns search <hostname>` — check if the name already exists.
|
||||||
|
2. `dns add <hostname> <ip>` — creates the A record with auto-PTR.
|
||||||
|
3. `dns get <hostname>` — verify resolution.
|
||||||
|
4. `dns flush` — flush cache so the new record is immediately resolvable.
|
||||||
|
|
||||||
|
### Pattern: before you act
|
||||||
|
- Always `dns search <pattern>` or `dns list` before adding/deleting —
|
||||||
|
confirm current state so you don't create duplicates or delete the wrong
|
||||||
|
record.
|
||||||
|
- **Never delete a record** unless the user explicitly asks.
|
||||||
|
- PTR records are auto-created on `add` and auto-deleted on `delete`.
|
||||||
|
|
||||||
|
## Git workflow
|
||||||
|
|
||||||
|
### Atomic commits
|
||||||
|
Each commit is **one logical change**. Stage precisely (`git add <file>`,
|
||||||
|
not `git add -A`).
|
||||||
|
|
||||||
|
### Conventional commit messages
|
||||||
|
```
|
||||||
|
<type>(<optional scope>): <imperative subject>
|
||||||
|
|
||||||
|
<optional body — why, not what>
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `docs`, `refactor`, `chore`, `style`.
|
||||||
|
|
||||||
|
- Subject line under 72 chars, lowercase, imperative mood.
|
||||||
|
- No period at end of subject.
|
||||||
|
- Body wrapped at 72 chars, explains **why** the change exists.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache bash curl python3 bind-tools
|
||||||
|
|
||||||
|
COPY src/dns_cli.sh /usr/local/bin/dns-cli
|
||||||
|
RUN chmod +x /usr/local/bin/dns-cli
|
||||||
|
|
||||||
|
ENTRYPOINT ["dns-cli"]
|
||||||
|
CMD ["--help"]
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# dns-cli
|
||||||
|
|
||||||
|
A Docker container that lets an AI agent (or a human) manage DNS records on a
|
||||||
|
[Technitium DNS Server](https://technitium.com/dns/) instance through its REST
|
||||||
|
API. A small bash CLI built on `curl` + `python3` + `dig`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker on the host.
|
||||||
|
- A Technitium DNS Server instance with the REST API enabled.
|
||||||
|
- A DNS API token (Technitium → Settings → API Token).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Credentials live in the **centralized credential store** at
|
||||||
|
`~/.creds/technitium.env` (see `tooling-cli/KNELCredsManager`). It holds:
|
||||||
|
|
||||||
|
```
|
||||||
|
TECHNITIUM_URL=http://pfv-netinfra-01:5380
|
||||||
|
TECHNITIUM_DNS_TOKEN=your-api-token
|
||||||
|
TECHNITIUM_DNS_ZONE=knel.net
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `.env.example` in this directory as a template if you need to create one.
|
||||||
|
Permissions: directory `700`, the env file `600` (owner read/write only).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t git.knownelement.com/reachableceo/dns-cli:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
The image is also available in the Gitea registry:
|
||||||
|
|
||||||
|
```
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Invoke the container directly with `docker run`. Pass credentials from the
|
||||||
|
centralized store via `--env-file`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest zones
|
||||||
|
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest list
|
||||||
|
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest search ns1
|
||||||
|
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest add myhost 192.168.1.50
|
||||||
|
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest delete myhost 192.168.1.50
|
||||||
|
|
||||||
|
docker run --rm --env-file ~/.creds/technitium.env \
|
||||||
|
git.knownelement.com/reachableceo/dns-cli:latest flush
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `zones` | List all zones. |
|
||||||
|
| `list [zone]` | List records in a zone (default: `TECHNITIUM_DNS_ZONE`). |
|
||||||
|
| `get <name>` | Resolve a record via `dig` (A/AAAA). |
|
||||||
|
| `add <name> <ip> [ttl]` | Add an A record with auto-PTR (default TTL 3600). |
|
||||||
|
| `delete <name> <ip>` | Delete an A record. |
|
||||||
|
| `flush` | Flush the DNS cache. |
|
||||||
|
| `search <pattern>` | Search records by name pattern (case-insensitive). |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `TECHNITIUM_URL` | yes | Base URL of the Technitium instance. |
|
||||||
|
| `TECHNITIUM_DNS_TOKEN` | yes | API token for the DNS REST API. |
|
||||||
|
| `TECHNITIUM_DNS_ZONE` | yes | Default zone for add/delete/search operations. |
|
||||||
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# dns-cli - CLI wrapper for Technitium DNS Server API
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# dns-cli zones List all zones
|
||||||
|
# dns-cli list [zone] List records in a zone (default: $TECHNITIUM_DNS_ZONE)
|
||||||
|
# dns-cli get <name> Resolve a record (A/AAAA)
|
||||||
|
# dns-cli add <name> <ip> [ttl] Add A record (+ auto PTR)
|
||||||
|
# dns-cli delete <name> <ip> Delete A record
|
||||||
|
# dns-cli flush Flush DNS cache
|
||||||
|
# dns-cli search <pattern> Search records by name pattern
|
||||||
|
#
|
||||||
|
# Env (provided via --env-file ~/.creds/technitium.env):
|
||||||
|
# TECHNITIUM_URL Technitium base URL (required)
|
||||||
|
# TECHNITIUM_DNS_TOKEN API token (required)
|
||||||
|
# TECHNITIUM_DNS_ZONE Default zone (required)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
URL="${TECHNITIUM_URL:?TECHNITIUM_URL is required}"
|
||||||
|
TOKEN="${TECHNITIUM_DNS_TOKEN:?TECHNITIUM_DNS_TOKEN is required}"
|
||||||
|
ZONE="${TECHNITIUM_DNS_ZONE:?TECHNITIUM_DNS_ZONE is required}"
|
||||||
|
|
||||||
|
# API helper — adds Bearer auth, returns JSON
|
||||||
|
api() {
|
||||||
|
local method="$1" endpoint="$2"; shift 2
|
||||||
|
local args=()
|
||||||
|
for kv in "$@"; do
|
||||||
|
args+=(-d "$kv")
|
||||||
|
done
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" -X "$method" "${args[@]}" \
|
||||||
|
"$URL/api/$endpoint" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Subcommands
|
||||||
|
cmd_zones() {
|
||||||
|
api GET "zones/list" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
for z in d.get('response',{}).get('zones',[]):
|
||||||
|
print(z['name'])
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local zone="${1:-$ZONE}"
|
||||||
|
api GET "zones/records/get?domain=$zone&zone=$zone&listZone=true" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
recs = d.get('response',{}).get('records',[])
|
||||||
|
for r in sorted(recs, key=lambda x: (x['name'], x['type'])):
|
||||||
|
rd = r.get('rData',{})
|
||||||
|
ip = rd.get('ipAddress','')
|
||||||
|
val = ip or rd.get('nameServer','') or rd.get('primaryNameServer','') or str(rd)
|
||||||
|
print(f\"{r['name']:50s} {r['type']:6s} {r.get('ttl',''):6} {val}\")
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_get() {
|
||||||
|
local name="$1"
|
||||||
|
dig +short "$name" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_add() {
|
||||||
|
local name="$1" ip="$2" ttl="${3:-3600}"
|
||||||
|
local fqdn="$name"
|
||||||
|
[[ "$fqdn" != *.* ]] && fqdn="$name.$ZONE"
|
||||||
|
api POST "zones/records/add" \
|
||||||
|
"zone=$ZONE" "domain=$fqdn" "type=A" "ttl=$ttl" "ipAddress=$ip" "ptr=true" "overwrite=true" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(d.get('status','?'), d.get('errorMessage',''))
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_delete() {
|
||||||
|
local name="$1" ip="$2"
|
||||||
|
local fqdn="$name"
|
||||||
|
[[ "$fqdn" != *.* ]] && fqdn="$name.$ZONE"
|
||||||
|
api POST "zones/records/delete" \
|
||||||
|
"zone=$ZONE" "domain=$fqdn" "type=A" "ipAddress=$ip" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(d.get('status','?'), d.get('errorMessage',''))
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_flush() {
|
||||||
|
api POST "cache/flush" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(d.get('status','?'))
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_search() {
|
||||||
|
local pattern="$1"
|
||||||
|
api GET "zones/records/get?domain=$ZONE&zone=$ZONE&listZone=true" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
recs = d.get('response',{}).get('records',[])
|
||||||
|
for r in sorted(recs, key=lambda x: x['name']):
|
||||||
|
if '$pattern' in r['name'].lower():
|
||||||
|
rd = r.get('rData',{})
|
||||||
|
ip = rd.get('ipAddress','')
|
||||||
|
val = ip or rd.get('nameServer','') or str(rd)
|
||||||
|
print(f\"{r['name']:50s} {r['type']:6s} {val}\")
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
case "${1:-}" in
|
||||||
|
zones) cmd_zones ;;
|
||||||
|
list) shift; cmd_list "${1:-}" ;;
|
||||||
|
get) shift; cmd_get "$1" ;;
|
||||||
|
add) shift; cmd_add "$@" ;;
|
||||||
|
delete) shift; cmd_delete "$@" ;;
|
||||||
|
flush) cmd_flush ;;
|
||||||
|
search) shift; cmd_search "$1" ;;
|
||||||
|
""|-h|--help|help)
|
||||||
|
sed -n '2,18p' "$0" >&2
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown command: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# dns-cli validation script.
|
||||||
|
# Runs a live read-only cycle against the configured Technitium instance.
|
||||||
|
#
|
||||||
|
# Usage: ./validate.sh
|
||||||
|
# Exit codes: 0 = all checks passed, 1 = one or more checks failed
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
ENV_FILE="${TECHNITIUM_ENV_FILE:-$HOME/.creds/technitium.env}"
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "FAIL: env file not found: $ENV_FILE"
|
||||||
|
echo " Set TECHNITIUM_ENV_FILE or create it in ~/.creds/technitium.env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGE="${DNS_CLI_IMAGE:-git.knownelement.com/reachableceo/dns-cli:latest}"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
TOTAL=0
|
||||||
|
|
||||||
|
ok() { TOTAL=$((TOTAL+1)); echo "PASS: $1"; PASS=$((PASS+1)); }
|
||||||
|
fail() { TOTAL=$((TOTAL+1)); echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo " dns-cli validation"
|
||||||
|
echo " image: ${IMAGE}"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
# 1. zones (live read)
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
echo "--- [1] zones ---"
|
||||||
|
OUT=$(docker run --rm --env-file "$ENV_FILE" "${IMAGE}" zones 2>&1) || true
|
||||||
|
if echo "$OUT" | grep -qE '\.'; then
|
||||||
|
ok "zones returned data"
|
||||||
|
echo "$OUT" | head -5
|
||||||
|
else
|
||||||
|
fail "zones did not return expected output"
|
||||||
|
echo " $OUT" | head -5
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
# 2. list (live read)
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
echo "--- [2] list ---"
|
||||||
|
OUT=$(docker run --rm --env-file "$ENV_FILE" "${IMAGE}" list 2>&1) || true
|
||||||
|
if echo "$OUT" | grep -qE 'A\s'; then
|
||||||
|
ok "list returned records"
|
||||||
|
echo "$OUT" | head -5
|
||||||
|
else
|
||||||
|
fail "list did not return expected output"
|
||||||
|
echo " $OUT" | head -5
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
# 3. search (live read)
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
echo "--- [3] search ---"
|
||||||
|
OUT=$(docker run --rm --env-file "$ENV_FILE" "${IMAGE}" search "ns" 2>&1) || true
|
||||||
|
if [ -n "$OUT" ]; then
|
||||||
|
ok "search returned results"
|
||||||
|
echo "$OUT" | head -3
|
||||||
|
else
|
||||||
|
fail "search returned no results"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
# Results
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
echo "============================================"
|
||||||
|
echo " RESULTS: ${PASS}/${TOTAL} passed, ${FAIL} failed"
|
||||||
|
echo "============================================"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Reference in New Issue
Block a user