13 Commits
Author SHA1 Message Date
warkanum 917480670f chore(release): update package version to 0.0.10
Release / test (push) Successful in 36s
Release / release (push) Successful in 44s
Release / pkg-deb (push) Successful in 38s
Release / pkg-rpm (push) Successful in 48s
Release / pkg-aur (push) Successful in 56s
2026-07-05 15:12:36 +02:00
warkanum 029b9cee2e feat(cmd): add installall/uninstallall; fix startall/stopall and service lifecycle
- startall now uses systemctl start (not enable --now)
- stopall now uses systemctl stop (not disable --now)
- add installall command (bulk install with --dry-run support)
- add uninstallall command (bulk disable+remove in reverse order)
- fix docker ExecStart to use -a flag so the process stays attached
- prefix ExecStop with - so a stop-on-already-dead container does not fail the unit
2026-07-05 15:12:18 +02:00
warkanum e6ef6e11d6 chore(release): update package version to 0.0.9
Release / test (push) Successful in -33m2s
Release / release (push) Successful in -32m47s
Release / pkg-aur (push) Successful in -33m46s
Release / pkg-deb (push) Successful in -33m19s
Release / pkg-rpm (push) Successful in -32m33s
2026-04-12 11:08:47 +02:00
warkanum f9dcb0b561 Merge branch 'main' of git.warky.dev:wdevs/unitdore 2026-04-12 11:08:38 +02:00
warkanum 69069a2196 fix(podman): handle invalid JSON output gracefully 2026-04-12 11:08:20 +02:00
Hein 2efccc5d4f chore(release): update package version to 0.0.8
Release / test (push) Successful in -30m10s
Release / release (push) Successful in -30m3s
Release / pkg-aur (push) Successful in -29m57s
Release / pkg-rpm (push) Failing after 14m44s
Release / pkg-deb (push) Failing after 14m51s
2026-04-10 09:38:59 +02:00
Hein b58373ad2f feat(config): add restart options for unit configuration
* include Restart, RestartSec, and RestartRetries fields
* update service file generation to support restart settings
* add tests for restart behavior in unit generation
2026-04-10 09:38:45 +02:00
warkanum a273232303 chore(release): update package version to 0.0.7
Release / release (push) Successful in -30m29s
Release / pkg-aur (push) Successful in -30m19s
Release / test (push) Successful in -30m43s
Release / pkg-deb (push) Successful in -30m29s
Release / pkg-rpm (push) Successful in -29m17s
2026-04-08 19:59:11 +02:00
warkanum fae49a258c ci(release): improve RPM upload logic in release workflow 2026-04-08 19:59:00 +02:00
warkanum 00bc6ed893 chore(release): update package version to 0.0.6
Release / test (push) Successful in -30m39s
Release / release (push) Successful in -30m31s
Release / pkg-deb (push) Successful in -30m33s
Release / pkg-aur (push) Successful in -29m41s
Release / pkg-rpm (push) Failing after -29m26s
2026-04-08 19:48:22 +02:00
warkanum b44546dc24 docs(README): update installation instructions for various distributions 2026-04-08 19:48:07 +02:00
Hein c6198ea6b7 chore(release): update package version to 0.0.4 and fix build paths
Release / pkg-deb (push) Successful in -30m35s
Release / pkg-aur (push) Successful in -30m17s
Release / pkg-rpm (push) Failing after -29m39s
Release / test (push) Successful in -30m39s
Release / release (push) Successful in -30m33s
* Adjust pkgver in PKGBUILD to 0.0.4
* Update build paths in PKGBUILD for consistency
* Enhance AUR package versioning logic in release workflow
2026-04-08 17:50:15 +02:00
Hein 9ef31866f1 chore(release): update curl installation in Rocky 9 image 2026-04-08 17:35:12 +02:00
23 changed files with 554 additions and 104 deletions
+60 -18
View File
@@ -48,7 +48,7 @@ jobs:
[ "$GOOS" = "windows" ] && EXT=".exe" [ "$GOOS" = "windows" ] && EXT=".exe"
NAME="unitdore-${GOOS}-${GOARCH}${EXT}" NAME="unitdore-${GOOS}-${GOARCH}${EXT}"
GOOS="$GOOS" GOARCH="$GOARCH" go build \ GOOS="$GOOS" GOARCH="$GOARCH" go build \
-ldflags "-X main.version=${VERSION}" \ -ldflags "-X github.com/warkanum/unitdore/cmd.version=${VERSION}" \
-o "$NAME" . -o "$NAME" .
echo "Built $NAME" echo "Built $NAME"
done done
@@ -146,12 +146,31 @@ jobs:
GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$AUR_KNOWN_HOSTS -i $AUR_KEY_PATH" \ GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$AUR_KNOWN_HOSTS -i $AUR_KEY_PATH" \
git clone ssh://aur@aur.archlinux.org/unitdore.git aur-repo git clone ssh://aur@aur.archlinux.org/unitdore.git aur-repo
# Compute SHA256 of the release tarball (same URL the PKGBUILD will download) CURRENT_PKGVER=$(awk -F= '/^pkgver=/ {print $2; exit}' aur-repo/PKGBUILD | tr -d "[:space:]")
CURRENT_PKGREL=$(awk -F= '/^pkgrel=/ {print $2; exit}' aur-repo/PKGBUILD | tr -d "[:space:]")
if [ "$CURRENT_PKGVER" = "$PKGVER" ]; then
case "$CURRENT_PKGREL" in
''|*[!0-9]*)
echo "Unsupported pkgrel in AUR repo: ${CURRENT_PKGREL}"
exit 1
;;
*)
PKGREL=$((CURRENT_PKGREL + 1))
;;
esac
else
PKGREL=1
fi
echo "Publishing AUR package version ${PKGVER}-${PKGREL}"
# Compute SHA256 of the source archive from the same URL the PKGBUILD will download.
SHA=$(curl -fsSL "https://git.warky.dev/wdevs/unitdore/archive/v${PKGVER}.zip" | sha256sum | cut -d' ' -f1) SHA=$(curl -fsSL "https://git.warky.dev/wdevs/unitdore/archive/v${PKGVER}.zip" | sha256sum | cut -d' ' -f1)
# Update PKGBUILD — keep remote source URL, only bump version and checksum # Update PKGBUILD — keep remote source URL, bump version/checksum, and increment pkgrel for same-version rebuilds.
sed -e "s/^pkgver=.*/pkgver=${PKGVER}/" \ sed -e "s/^pkgver=.*/pkgver=${PKGVER}/" \
-e "s/^pkgrel=.*/pkgrel=1/" \ -e "s/^pkgrel=.*/pkgrel=${PKGREL}/" \
-e "s/^sha256sums=.*/sha256sums=('${SHA}')/" \ -e "s/^sha256sums=.*/sha256sums=('${SHA}')/" \
pkg/arch/PKGBUILD > aur-repo/PKGBUILD pkg/arch/PKGBUILD > aur-repo/PKGBUILD
@@ -172,7 +191,7 @@ jobs:
git config user.email "hein@warky.dev" git config user.email "hein@warky.dev"
git config user.name "Hein" git config user.name "Hein"
git add PKGBUILD .SRCINFO git add PKGBUILD .SRCINFO
git commit -m "Update to v${PKGVER}" git commit -m "Update to v${PKGVER}-${PKGREL}"
GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$AUR_KNOWN_HOSTS -i $AUR_KEY_PATH" \ GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$AUR_KNOWN_HOSTS -i $AUR_KEY_PATH" \
git push origin HEAD:master git push origin HEAD:master
@@ -196,7 +215,7 @@ jobs:
for GOARCH in amd64 arm64; do for GOARCH in amd64 arm64; do
GOOS=linux GOARCH=$GOARCH go build \ GOOS=linux GOARCH=$GOARCH go build \
-trimpath \ -trimpath \
-ldflags "-X main.version=${PKGVER}" \ -ldflags "-X github.com/warkanum/unitdore/cmd.version=${PKGVER}" \
-o unitdore . -o unitdore .
PKGDIR="unitdore_${PKGVER}_${GOARCH}" PKGDIR="unitdore_${PKGVER}_${GOARCH}"
@@ -243,8 +262,16 @@ jobs:
- name: Build RPM - name: Build RPM
run: | run: |
set -euo pipefail
VERSION="${{ github.event.inputs.tag || github.ref_name }}" VERSION="${{ github.event.inputs.tag || github.ref_name }}"
PKGVER="${VERSION#v}" PKGVER="${VERSION#v}"
GO_VER="$(awk '/^go / { print $2; exit }' go.mod)"
if [ -z "${GO_VER}" ]; then
echo "Failed to determine Go version from go.mod"
exit 1
fi
# Source tarball — prefix=unitdore-VERSION/ matches RPM %autosetup convention # Source tarball — prefix=unitdore-VERSION/ matches RPM %autosetup convention
git archive --format=tar.gz --prefix=unitdore-${PKGVER}/ HEAD \ git archive --format=tar.gz --prefix=unitdore-${PKGVER}/ HEAD \
@@ -254,22 +281,37 @@ jobs:
sed -i "s/^Version:.*/Version: ${PKGVER}/" pkg/centos/unitdore.spec sed -i "s/^Version:.*/Version: ${PKGVER}/" pkg/centos/unitdore.spec
mkdir -p pkg/centos/out mkdir -p pkg/centos/out
docker run --rm \ CID=$(docker create \
-v "$PWD:/workspace" \ -e GO_VER="${GO_VER}" \
-v "$PWD/pkg/centos/out:/out" \ -e PKGVER="${PKGVER}" \
-w /workspace \ -w /build \
rockylinux:9 \ rockylinux:9 \
bash -c " bash -lc "
dnf install -y rpm-build git curl && set -euo pipefail
GO_VER=\$(grep '^go ' /workspace/go.mod | awk '{print \$2}') && # Rocky 9 images already ship curl-minimal, which is enough for the Go tarball download.
dnf install -y rpm-build git &&
curl -fsSL https://go.dev/dl/go\${GO_VER}.linux-amd64.tar.gz | tar -C /usr/local -xz && curl -fsSL https://go.dev/dl/go\${GO_VER}.linux-amd64.tar.gz | tar -C /usr/local -xz &&
export PATH=\$PATH:/usr/local/go/bin && export PATH=\$PATH:/usr/local/go/bin &&
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} && mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} &&
cp unitdore-${PKGVER}.tar.gz ~/rpmbuild/SOURCES/ && cp unitdore-${PKGVER}.tar.gz ~/rpmbuild/SOURCES/ &&
cp pkg/centos/unitdore.spec ~/rpmbuild/SPECS/ && cp pkg/centos/unitdore.spec ~/rpmbuild/SPECS/ &&
rpmbuild --nodeps -ba ~/rpmbuild/SPECS/unitdore.spec && rpmbuild --nodeps -ba ~/rpmbuild/SPECS/unitdore.spec
find ~/rpmbuild/RPMS -name '*.rpm' -exec cp {} /out/ \; ")
"
cleanup() {
docker rm -f "$CID" >/dev/null 2>&1 || true
}
trap cleanup EXIT
# Avoid bind mounts here because DinD runners may not expose the checkout path to the Docker daemon.
docker cp unitdore-${PKGVER}.tar.gz "$CID:/build/unitdore-${PKGVER}.tar.gz"
docker cp pkg "$CID:/build/pkg"
docker start -a "$CID"
docker cp "$CID:/root/rpmbuild/RPMS/." pkg/centos/out/
trap - EXIT
cleanup
- name: Upload to release - name: Upload to release
run: | run: |
@@ -277,13 +319,13 @@ jobs:
RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \ RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
-H "Authorization: token ${GITHUB_TOKEN}") -H "Authorization: token ${GITHUB_TOKEN}")
UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4) UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4)
for f in pkg/centos/out/*.rpm; do while IFS= read -r f; do
FNAME=$(basename "$f") FNAME=$(basename "$f")
echo "Uploading $FNAME..." echo "Uploading $FNAME..."
curl -s -X POST "${UPLOAD_URL}?name=${FNAME}" \ curl -s -X POST "${UPLOAD_URL}?name=${FNAME}" \
-H "Authorization: token ${GITHUB_TOKEN}" \ -H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary "@${f}" > /dev/null --data-binary "@${f}" > /dev/null
done done < <(find pkg/centos/out -name "*.rpm")
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1
View File
@@ -1,2 +1,3 @@
unitdore unitdore
*.exe *.exe
.codex
+92
View File
@@ -0,0 +1,92 @@
## Project Overview
**Unitdore** is a Go CLI tool that bridges container runtimes (Podman, Docker) and systemd. It discovers running containers, stores them in a YAML config, and generates + manages systemd `.service` unit files for each one.
# Agent Rules
Keep answers short. Question everything, never assume or guess. Ask if unsure.
You must use the AMCS MCP system tools. Set **unitdore** as the active project and always capture summaries of what was done as thoughts using the `capture_thought` tool. If AMCS is not available, write files to `docs/llm/log/yyyymmdd_hh.md`.
# Tools to use
When writing Go: `doc/tools/go-skill.md` (if present), otherwise follow standard Go conventions.
## Commands
```bash
make build # Compile binary: ./unitdore
make install # Install binary to /usr/bin, man page, create /etc/unitdore
make uninstall # Remove binary and man page
make test # go test ./... -v
make test-short # go test ./...
make lint # go vet ./...
make clean # Remove built binary
make release-version # Bump patch version, commit, tag, and push
```
## Architecture
```
cmd/ # Cobra commands (edit, install, list, root, start, startall,
# status, stop, stopall, syncup, uninstall, update)
config/ # Config struct + YAML load/save (units.yaml)
runtime/ # Docker and Podman runtime abstraction
systemd/ # .service file generator and systemd manager
docs/ # generated-units.md (unit file examples), unitdore.1 (man page)
```
### Key packages
- **`config`** — `Unit` and `Config` structs; `Load`, `Save`, `FindUnit`, `AddUnit`
- **`runtime`** — `ContainerRuntime` interface; `docker.go`, `podman.go` implementations
- **`systemd`** — `generator.go` builds `.service` file content; `manager.go` calls `systemctl`
### Config file
**Location:** `/etc/unitdore/units.yaml`
```yaml
prefix: "" # prepended to service file name (e.g. "prod-")
suffix: "" # appended to service file name (e.g. "-svc")
units:
- name: nginx
runtime: podman # podman | docker
user: "" # empty = system unit; set for rootless user unit
command: "" # override ExecStart entirely
order: 1 # startup order, lower = earlier; same = parallel
delay: 0s # delay after previous order group
enabled: true
disabled_reason: "" # auto-set by syncup reconciliation
restart: false # Restart=on-failure
restart_sec: 0 # RestartSec
restart_retries: 0 # StartLimitBurst
```
### Generated service file locations
| Scenario | Path |
|---|---|
| System unit (root) | `/etc/systemd/system/unitdore-<name>.service` |
| User unit | `/home/<user>/.config/systemd/user/unitdore-<name>.service` |
| With prefix `prod-` | `/etc/systemd/system/unitdore-prod-<name>.service` |
| With suffix `-svc` | `/etc/systemd/system/unitdore-<name>-svc.service` |
### Libraries
- `github.com/spf13/cobra` — CLI framework
- `gopkg.in/yaml.v3` — config parsing
## Testing
Tests exist in:
- `config/config_test.go`
- `runtime/runtime_test.go`
- `systemd/generator_test.go`
Always run `make test` before marking a task complete. Add tests for new behaviour in the same package as the code under test.
## Additional documentation
- `docs/generated-units.md` — full annotated examples of every generated unit file combination
- `docs/unitdore.1` — man page
+35
View File
@@ -0,0 +1,35 @@
# AI Usage Declaration
This Go project utilizes AI tools for the following purposes:
- Generating and improving documentation
- Writing and enhancing tests
- Refactoring and optimizing existing code
AI is **not** used for core design or architecture decisions.
All design decisions are deferred to human discussion.
AI is employed only for enhancements to human-written code.
We are aware of significant AI hallucinations; all AI-generated content is to be reviewed and verified by humans.
.-""""""-.
.' '.
/ O O \
: ` :
| |
: .------. :
\ ' ' /
'. .'
'-......-'
MEGAMIND AI
[============]
___________
/___________\
/_____________\
| ASSIMILATE |
| RESISTANCE |
| IS FUTILE |
\_____________/
\___________/
+5
View File
@@ -0,0 +1,5 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
You must now read the./AGENTS.md
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Hein (Warky Devs)
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.
+11 -5
View File
@@ -1,8 +1,8 @@
BINARY := unitdore BINARY := unitdore
INSTALL_DIR := /usr/local/bin INSTALL_DIR := /usr/bin
CONFIG_DIR := /etc/unitdore CONFIG_DIR := /etc/unitdore
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
LDFLAGS := -ldflags "-X main.version=$(VERSION)" LDFLAGS := -ldflags "-X github.com/warkanum/unitdore/cmd.version=$(VERSION)"
.PHONY: all build install uninstall test lint clean release-version .PHONY: all build install uninstall test lint clean release-version
@@ -42,17 +42,23 @@ lint:
clean: clean:
rm -f $(BINARY) rm -f $(BINARY)
## release-version: bump patch version, tag, and push to trigger release workflow ## release-version: bump patch version, update source versions, commit, tag, and push
release-version: release-version:
@CURRENT=$$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"); \ @CURRENT=$$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"); \
MAJOR=$$(echo $$CURRENT | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1/'); \ MAJOR=$$(echo $$CURRENT | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1/'); \
MINOR=$$(echo $$CURRENT | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\2/'); \ MINOR=$$(echo $$CURRENT | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\2/'); \
PATCH=$$(echo $$CURRENT | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\3/'); \ PATCH=$$(echo $$CURRENT | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\3/'); \
NEXT="v$$MAJOR.$$MINOR.$$((PATCH + 1))"; \ NEXT="v$$MAJOR.$$MINOR.$$((PATCH + 1))"; \
PKGVER="$$MAJOR.$$MINOR.$$((PATCH + 1))"; \
echo "Current: $$CURRENT → Next: $$NEXT"; \ echo "Current: $$CURRENT → Next: $$NEXT"; \
sed -i "s/^var version = .*/var version = \"$$PKGVER\"/" cmd/root.go; \
sed -i "s/^pkgver=.*/pkgver=$$PKGVER/" pkg/arch/PKGBUILD; \
sed -i "s/^Version:.*/Version: $$PKGVER/" pkg/centos/unitdore.spec; \
git add cmd/root.go pkg/arch/PKGBUILD pkg/centos/unitdore.spec; \
git commit -m "chore(release): update package version to $$PKGVER"; \
git tag -a "$$NEXT" -m "Release $$NEXT"; \ git tag -a "$$NEXT" -m "Release $$NEXT"; \
git push origin "$$NEXT"; \ git push origin HEAD "$$NEXT"; \
echo "Pushed tag $$NEXT — release workflow triggered" echo "Pushed $$NEXT — release workflow triggered"
## help: show this help ## help: show this help
help: help:
+26
View File
@@ -6,6 +6,32 @@ Unitdore bridges your container runtime (Podman, Docker) and systemd. It discove
## Install ## Install
**Arch Linux (AUR)**
```bash
yay -S unitdore
```
**Debian / Ubuntu**
Download the `.deb` from the [releases page](https://git.warky.dev/wdevs/unitdore/releases) and install:
```bash
sudo dpkg -i unitdore_*.deb
```
**CentOS / AlmaLinux / Rocky Linux**
Download the `.rpm` from the [releases page](https://git.warky.dev/wdevs/unitdore/releases) and install:
```bash
sudo rpm -i unitdore-*.rpm
# or with dnf:
sudo dnf install unitdore-*.rpm
```
**From source**
```bash ```bash
make build make build
sudo make install sudo make install
+3 -3
View File
@@ -31,7 +31,7 @@ func runInstall(cmd *cobra.Command, args []string) error {
return err return err
} }
prefix, suffix, serviceUser := cfg.Prefix, cfg.Suffix, cfg.EffectiveServiceUser() prefix, suffix := cfg.Prefix, cfg.Suffix
installed := 0 installed := 0
skipped := 0 skipped := 0
removed := 0 removed := 0
@@ -57,7 +57,7 @@ func runInstall(cmd *cobra.Command, args []string) error {
} }
if dryRun { if dryRun {
content, err := systemd.Generate(u, prefix, suffix, serviceUser) content, err := systemd.Generate(u, prefix, suffix)
if err != nil { if err != nil {
fmt.Printf(" ✗ %s: %v\n", u.Name, err) fmt.Printf(" ✗ %s: %v\n", u.Name, err)
continue continue
@@ -68,7 +68,7 @@ func runInstall(cmd *cobra.Command, args []string) error {
continue continue
} }
if err := systemd.Install(u, prefix, suffix, serviceUser); err != nil { if err := systemd.Install(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err) fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
} else { } else {
path, _ := systemd.UnitPath(u, prefix, suffix) path, _ := systemd.UnitPath(u, prefix, suffix)
+83
View File
@@ -0,0 +1,83 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
)
var installallDryRun bool
var installallCmd = &cobra.Command{
Use: "installall",
Short: "Generate and install systemd unit files for all enabled units",
Long: `Installall generates .service files for all enabled units and writes them
to the appropriate systemd directory. It does not enable or start them.
Use --dry-run to preview the generated unit files without writing anything.`,
RunE: runInstallall,
}
func init() {
installallCmd.Flags().BoolVar(&installallDryRun, "dry-run", false, "preview unit files without writing")
rootCmd.AddCommand(installallCmd)
}
func runInstallall(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
prefix, suffix := cfg.Prefix, cfg.Suffix
installed := 0
skipped := 0
removed := 0
for _, u := range cfg.Units {
if !u.Enabled {
if systemd.IsInstalled(u, prefix, suffix) {
if installallDryRun {
fmt.Printf(" ~ would remove: %s\n", systemd.ServiceName(u, prefix, suffix))
} else {
if err := systemd.Uninstall(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
} else {
fmt.Printf(" - removed: %s (disabled)\n", systemd.ServiceName(u, prefix, suffix))
removed++
}
}
} else {
skipped++
}
continue
}
if installallDryRun {
content, err := systemd.Generate(u, prefix, suffix)
if err != nil {
fmt.Printf(" ✗ %s: %v\n", u.Name, err)
continue
}
path, _ := systemd.UnitPath(u, prefix, suffix)
fmt.Printf("\n--- %s ---\n%s\n", path, content)
installed++
continue
}
if err := systemd.Install(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
} else {
path, _ := systemd.UnitPath(u, prefix, suffix)
fmt.Printf(" ✓ installed: %s\n", path)
installed++
}
}
if !installallDryRun {
fmt.Printf("\nDone. Installed: %d Removed: %d Skipped: %d\n", installed, removed, skipped)
}
return nil
}
+3 -1
View File
@@ -7,6 +7,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var version = "0.0.10"
var configPath string var configPath string
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -16,7 +18,7 @@ var rootCmd = &cobra.Command{
It discovers running containers, stores them in a config file, It discovers running containers, stores them in a config file,
and generates + manages systemd .service units for each one.`, and generates + manages systemd .service units for each one.`,
Version: "0.1.0", Version: version,
} }
func Execute() { func Execute() {
+4 -4
View File
@@ -11,9 +11,9 @@ import (
var startallCmd = &cobra.Command{ var startallCmd = &cobra.Command{
Use: "startall", Use: "startall",
Short: "Enable and start all installed, enabled units", Short: "Start all installed, enabled units",
Long: `Startall runs 'systemctl enable --now' for all enabled units that have Long: `Startall runs 'systemctl start' for all enabled units that have been installed.
been installed. Units must be installed first via 'unitdore install'.`, Units must be installed first via 'unitdore installall'.`,
RunE: runStartall, RunE: runStartall,
} }
@@ -50,7 +50,7 @@ func runStartall(cmd *cobra.Command, args []string) error {
continue continue
} }
fmt.Printf(" ▶ starting: %s...\n", systemd.ServiceName(u, prefix, suffix)) fmt.Printf(" ▶ starting: %s...\n", systemd.ServiceName(u, prefix, suffix))
if err := systemd.Enable(u, prefix, suffix); err != nil { if err := systemd.Start(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err) fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
failed++ failed++
} else { } else {
+3 -3
View File
@@ -11,8 +11,8 @@ import (
var stopallCmd = &cobra.Command{ var stopallCmd = &cobra.Command{
Use: "stopall", Use: "stopall",
Short: "Stop and disable all running managed units", Short: "Stop all installed units",
Long: `Stopall runs 'systemctl disable --now' for all enabled, installed units.`, Long: `Stopall runs 'systemctl stop' for all installed units in reverse startup order.`,
RunE: runStopall, RunE: runStopall,
} }
@@ -46,7 +46,7 @@ func runStopall(cmd *cobra.Command, args []string) error {
continue continue
} }
fmt.Printf(" ■ stopping: %s...\n", systemd.ServiceName(u, prefix, suffix)) fmt.Printf(" ■ stopping: %s...\n", systemd.ServiceName(u, prefix, suffix))
if err := systemd.Disable(u, prefix, suffix); err != nil { if err := systemd.Stop(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err) fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
failed++ failed++
} else { } else {
+67
View File
@@ -0,0 +1,67 @@
package cmd
import (
"fmt"
"sort"
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
)
var uninstallallCmd = &cobra.Command{
Use: "uninstallall",
Short: "Stop, disable, and remove service files for all installed units",
Long: `Uninstallall runs 'systemctl disable --now' and removes the .service file for every installed unit, in reverse startup order.`,
RunE: runUninstallall,
}
func init() {
rootCmd.AddCommand(uninstallallCmd)
}
func runUninstallall(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
prefix, suffix := cfg.Prefix, cfg.Suffix
units := make([]config.Unit, len(cfg.Units))
copy(units, cfg.Units)
sort.Slice(units, func(i, j int) bool {
if units[i].Order != units[j].Order {
return units[i].Order > units[j].Order
}
return units[i].Name > units[j].Name
})
removed := 0
failed := 0
for _, u := range units {
if !systemd.IsInstalled(u, prefix, suffix) {
continue
}
svcName := systemd.ServiceName(u, prefix, suffix)
fmt.Printf(" ■ disabling %s...\n", svcName)
if err := systemd.Disable(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to disable %s: %v\n", u.Name, err)
failed++
continue
}
path, _ := systemd.UnitPath(u, prefix, suffix)
fmt.Printf(" ✗ removing %s...\n", path)
if err := systemd.Uninstall(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
failed++
continue
}
fmt.Printf(" ✓ uninstalled: %s\n", u.Name)
removed++
}
fmt.Printf("\nDone. Uninstalled: %d Failed: %d\n", removed, failed)
return nil
}
+2 -2
View File
@@ -63,10 +63,10 @@ func runUpdate(cmd *cobra.Command, args []string) error {
return err return err
} }
prefix, suffix, serviceUser := cfg.Prefix, cfg.Suffix, cfg.EffectiveServiceUser() prefix, suffix := cfg.Prefix, cfg.Suffix
if systemd.IsInstalled(*u, prefix, suffix) { if systemd.IsInstalled(*u, prefix, suffix) {
if err := systemd.Install(*u, prefix, suffix, serviceUser); err != nil { if err := systemd.Install(*u, prefix, suffix); err != nil {
return fmt.Errorf("recreating service file: %w", err) return fmt.Errorf("recreating service file: %w", err)
} }
path, _ := systemd.UnitPath(*u, prefix, suffix) path, _ := systemd.UnitPath(*u, prefix, suffix)
+3 -13
View File
@@ -3,7 +3,6 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -21,6 +20,9 @@ type Unit struct {
Delay string `yaml:"delay,omitempty"` // e.g. "5s" Delay string `yaml:"delay,omitempty"` // e.g. "5s"
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
DisabledReason string `yaml:"disabled_reason,omitempty"` DisabledReason string `yaml:"disabled_reason,omitempty"`
Restart bool `yaml:"restart,omitempty"` // enable Restart=on-failure
RestartSec int `yaml:"restart_sec,omitempty"` // seconds between restarts
RestartRetries int `yaml:"restart_retries,omitempty"` // max restart attempts (StartLimitBurst)
} }
// Config is the root config structure. // Config is the root config structure.
@@ -28,18 +30,6 @@ type Config struct {
Units []Unit `yaml:"units"` Units []Unit `yaml:"units"`
Prefix string `yaml:"prefix,omitempty"` // prepended to generated service name, e.g. "prod-" Prefix string `yaml:"prefix,omitempty"` // prepended to generated service name, e.g. "prod-"
Suffix string `yaml:"suffix,omitempty"` // appended to generated service name, e.g. "-svc" Suffix string `yaml:"suffix,omitempty"` // appended to generated service name, e.g. "-svc"
ServiceUser string `yaml:"service_user,omitempty"` // User= in [Service] section; defaults to "unitdore"
}
// EffectiveServiceUser returns the configured service user, or the current OS user if unset.
func (c *Config) EffectiveServiceUser() string {
if c.ServiceUser != "" {
return c.ServiceUser
}
if u, err := user.Current(); err == nil {
return u.Username
}
return "root"
} }
// Load reads and parses the config file at path. // Load reads and parses the config file at path.
+5 -6
View File
@@ -1,6 +1,6 @@
# Maintainer: Hein (Warky Devs) <hein@warky.dev> # Maintainer: Hein (Warky Devs) <hein@warky.dev>
pkgname=unitdore pkgname=unitdore
pkgver=0.1.0 pkgver=0.0.10
pkgrel=1 pkgrel=1
pkgdesc="A door you open and close for container units — manage containers via systemd" pkgdesc="A door you open and close for container units — manage containers via systemd"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -12,26 +12,25 @@ optdepends=(
'docker: Docker container runtime support' 'docker: Docker container runtime support'
) )
makedepends=('go') makedepends=('go')
backup=('etc/unitdore/units.yaml')
source=("$pkgname-$pkgver.zip::$url/archive/v$pkgver.zip") source=("$pkgname-$pkgver.zip::$url/archive/v$pkgver.zip")
sha256sums=('SKIP') sha256sums=('SKIP')
build() { build() {
cd "$pkgname-v$pkgver" cd "$pkgname"
export CGO_ENABLED=0 export CGO_ENABLED=0
go build \ go build \
-trimpath \ -trimpath \
-ldflags "-X main.version=$pkgver" \ -ldflags "-X github.com/warkanum/unitdore/cmd.version=$pkgver" \
-o "$pkgname" . -o "$pkgname" .
} }
check() { check() {
cd "$pkgname-v$pkgver" cd "$pkgname"
go test ./... go test ./...
} }
package() { package() {
cd "$pkgname-v$pkgver" cd "$pkgname"
# Binary # Binary
install -Dm755 "$pkgname" "$pkgdir/usr/bin/$pkgname" install -Dm755 "$pkgname" "$pkgdir/usr/bin/$pkgname"
+7 -8
View File
@@ -1,5 +1,5 @@
Name: unitdore Name: unitdore
Version: 0.1.0 Version: 0.0.10
Release: 1%{?dist} Release: 1%{?dist}
Summary: Manage container units via systemd Summary: Manage container units via systemd
@@ -10,6 +10,8 @@ Source0: %{name}-%{version}.tar.gz
BuildRequires: golang >= 1.23 BuildRequires: golang >= 1.23
Requires: systemd Requires: systemd
%global debug_package %{nil}
%description %description
Unitdore bridges your container runtime (Podman, Docker) and systemd. Unitdore bridges your container runtime (Podman, Docker) and systemd.
It discovers running containers, stores them in a config file, and generates It discovers running containers, stores them in a config file, and generates
@@ -22,24 +24,21 @@ and manages systemd .service units for each one.
export CGO_ENABLED=0 export CGO_ENABLED=0
go build \ go build \
-trimpath \ -trimpath \
-ldflags "-X main.version=%{version}" \ -ldflags "-X github.com/warkanum/unitdore/cmd.version=%{version}" \
-o %{name} . -o %{name} .
%install %install
# Binary
install -Dm755 %{name} %{buildroot}%{_bindir}/%{name} install -Dm755 %{name} %{buildroot}%{_bindir}/%{name}
# Man page
install -Dm644 docs/unitdore.1 %{buildroot}%{_mandir}/man1/unitdore.1 install -Dm644 docs/unitdore.1 %{buildroot}%{_mandir}/man1/unitdore.1
install -Dm644 LICENSE %{buildroot}%{_licensedir}/%{name}/LICENSE
# Config dir
install -dm755 %{buildroot}%{_sysconfdir}/unitdore install -dm755 %{buildroot}%{_sysconfdir}/unitdore
%files %files
%license LICENSE
%{_bindir}/%{name} %{_bindir}/%{name}
%{_mandir}/man1/unitdore.1* %{_mandir}/man1/unitdore.1*
%dir %{_sysconfdir}/unitdore %dir %{_sysconfdir}/unitdore
%changelog %changelog
* Tue Apr 08 2026 Hein (Warky Devs) <hein@warky.dev> - 0.1.0-1 * Wed Apr 08 2026 Hein (Warky Devs) <hein@warky.dev> - 0.0.5-1
- Initial package - Initial package
+3
View File
@@ -2,6 +2,9 @@ Package: unitdore
Version: VERSION Version: VERSION
Architecture: ARCH Architecture: ARCH
Maintainer: Hein (Warky Devs) <hein@warky.dev> Maintainer: Hein (Warky Devs) <hein@warky.dev>
Section: admin
Priority: optional
Homepage: https://git.warky.dev/wdevs/unitdore
Depends: systemd Depends: systemd
Recommends: podman | docker.io Recommends: podman | docker.io
Description: A door you open and close for container units Description: A door you open and close for container units
+2 -1
View File
@@ -33,7 +33,8 @@ func (p *Podman) ListRunning() ([]Container, error) {
var raw []podmanContainer var raw []podmanContainer
if err := json.Unmarshal(out, &raw); err != nil { if err := json.Unmarshal(out, &raw); err != nil {
return nil, fmt.Errorf("parsing podman output: %w", err) // Podman installed but output is not valid JSON (e.g. OCI runtime misconfigured)
return nil, nil
} }
var containers []Container var containers []Container
+29 -10
View File
@@ -13,15 +13,20 @@ const systemUnitTemplate = `# /etc/systemd/system/{{.ServiceName}}
[Unit] [Unit]
Description=Unitdore: {{.Unit.Name}} ({{.Unit.Runtime}}) Description=Unitdore: {{.Unit.Name}} ({{.Unit.Runtime}})
After=network.target After=network.target{{with .RuntimeService}} {{.}}
Requires={{.}}{{end}}
[Service] [Service]
Type=simple Type=simple
User={{.ServiceUser}}
ExecStart={{.ExecStart}} ExecStart={{.ExecStart}}
ExecStop={{.ExecStop}} ExecStop={{if .ExecStop}}-{{.ExecStop}}{{end}}
{{- if .Unit.Restart}}
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec={{.Unit.RestartSec}}
{{- if gt .Unit.RestartRetries 0}}
StartLimitBurst={{.Unit.RestartRetries}}
{{- end}}
{{- end}}
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@@ -37,9 +42,14 @@ After=default.target
[Service] [Service]
Type=simple Type=simple
ExecStart={{.ExecStart}} ExecStart={{.ExecStart}}
ExecStop={{.ExecStop}} ExecStop={{if .ExecStop}}-{{.ExecStop}}{{end}}
{{- if .Unit.Restart}}
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec={{.Unit.RestartSec}}
{{- if gt .Unit.RestartRetries 0}}
StartLimitBurst={{.Unit.RestartRetries}}
{{- end}}
{{- end}}
[Install] [Install]
WantedBy=default.target WantedBy=default.target
@@ -50,7 +60,7 @@ type templateData struct {
Unit config.Unit Unit config.Unit
ExecStart string ExecStart string
ExecStop string ExecStop string
ServiceUser string RuntimeService string
} }
// ServiceName returns the systemd service name for a unit, with optional prefix/suffix. // ServiceName returns the systemd service name for a unit, with optional prefix/suffix.
@@ -59,7 +69,7 @@ func ServiceName(u config.Unit, prefix, suffix string) string {
} }
// Generate produces the .service file content for a unit. // Generate produces the .service file content for a unit.
func Generate(u config.Unit, prefix, suffix, serviceUser string) (string, error) { func Generate(u config.Unit, prefix, suffix string) (string, error) {
execStart, execStop := buildExecCommands(u) execStart, execStop := buildExecCommands(u)
data := templateData{ data := templateData{
@@ -67,7 +77,7 @@ func Generate(u config.Unit, prefix, suffix, serviceUser string) (string, error)
Unit: u, Unit: u,
ExecStart: execStart, ExecStart: execStart,
ExecStop: execStop, ExecStop: execStop,
ServiceUser: serviceUser, RuntimeService: runtimeService(u.Runtime),
} }
tmplStr := systemUnitTemplate tmplStr := systemUnitTemplate
@@ -87,6 +97,15 @@ func Generate(u config.Unit, prefix, suffix, serviceUser string) (string, error)
return buf.String(), nil return buf.String(), nil
} }
// runtimeService returns the systemd service name that must be running for the
// given container runtime, or "" if none is required (e.g. daemonless podman).
func runtimeService(runtime string) string {
if runtime == "docker" {
return "docker.service"
}
return ""
}
func buildExecCommands(u config.Unit) (start, stop string) { func buildExecCommands(u config.Unit) (start, stop string) {
// If a custom command is provided, use it directly // If a custom command is provided, use it directly
if u.Command != "" { if u.Command != "" {
@@ -98,7 +117,7 @@ func buildExecCommands(u config.Unit) (start, stop string) {
start = fmt.Sprintf("/usr/bin/podman start -a %s", u.Name) start = fmt.Sprintf("/usr/bin/podman start -a %s", u.Name)
stop = fmt.Sprintf("/usr/bin/podman stop %s", u.Name) stop = fmt.Sprintf("/usr/bin/podman stop %s", u.Name)
case "docker": case "docker":
start = fmt.Sprintf("/usr/bin/docker start %s", u.Name) start = fmt.Sprintf("/usr/bin/docker start -a %s", u.Name)
stop = fmt.Sprintf("/usr/bin/docker stop %s", u.Name) stop = fmt.Sprintf("/usr/bin/docker stop %s", u.Name)
default: default:
start = fmt.Sprintf("/usr/bin/%s start %s", u.Runtime, u.Name) start = fmt.Sprintf("/usr/bin/%s start %s", u.Runtime, u.Name)
+74 -15
View File
@@ -35,7 +35,7 @@ func TestGenerate_SystemUnit(t *testing.T) {
Enabled: true, Enabled: true,
} }
content, err := Generate(u, "", "", "unitdore") content, err := Generate(u, "", "")
if err != nil { if err != nil {
t.Fatalf("Generate() error: %v", err) t.Fatalf("Generate() error: %v", err)
} }
@@ -49,8 +49,7 @@ func TestGenerate_SystemUnit(t *testing.T) {
"After=network.target", "After=network.target",
"WantedBy=multi-user.target", "WantedBy=multi-user.target",
"ExecStart=/usr/bin/podman start -a nginx", "ExecStart=/usr/bin/podman start -a nginx",
"ExecStop=/usr/bin/podman stop nginx", "ExecStop=-/usr/bin/podman stop nginx",
"Restart=on-failure",
"Generated by unitdore", "Generated by unitdore",
} }
@@ -59,6 +58,16 @@ func TestGenerate_SystemUnit(t *testing.T) {
t.Errorf("Generate() missing %q in output:\n%s", check, content) t.Errorf("Generate() missing %q in output:\n%s", check, content)
} }
} }
if strings.Contains(content, "User=") {
t.Errorf("Generate() system unit should not contain User= directive:\n%s", content)
}
if strings.Contains(content, "Requires=") {
t.Errorf("Generate() podman system unit should not contain Requires= (podman is daemonless):\n%s", content)
}
if strings.Contains(content, "Restart=") {
t.Errorf("Generate() restart should be disabled by default:\n%s", content)
}
} }
func TestGenerate_UserUnit(t *testing.T) { func TestGenerate_UserUnit(t *testing.T) {
@@ -70,7 +79,7 @@ func TestGenerate_UserUnit(t *testing.T) {
Enabled: true, Enabled: true,
} }
content, err := Generate(u, "", "", "unitdore") content, err := Generate(u, "", "")
if err != nil { if err != nil {
t.Fatalf("Generate() error: %v", err) t.Fatalf("Generate() error: %v", err)
} }
@@ -78,8 +87,8 @@ func TestGenerate_UserUnit(t *testing.T) {
checks := []string{ checks := []string{
"After=default.target", "After=default.target",
"WantedBy=default.target", "WantedBy=default.target",
"ExecStart=/usr/bin/docker start myapp", "ExecStart=/usr/bin/docker start -a myapp",
"ExecStop=/usr/bin/docker stop myapp", "ExecStop=-/usr/bin/docker stop myapp",
} }
for _, check := range checks { for _, check := range checks {
@@ -93,6 +102,51 @@ func TestGenerate_UserUnit(t *testing.T) {
} }
} }
func TestGenerate_WithRestart(t *testing.T) {
t.Run("restart with retries", func(t *testing.T) {
u := config.Unit{
Name: "nginx",
Runtime: "podman",
Enabled: true,
Restart: true,
RestartSec: 5,
RestartRetries: 3,
}
content, err := Generate(u, "", "")
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
for _, want := range []string{"Restart=on-failure", "RestartSec=5", "StartLimitBurst=3"} {
if !strings.Contains(content, want) {
t.Errorf("Generate() missing %q in output:\n%s", want, content)
}
}
})
t.Run("restart without retries", func(t *testing.T) {
u := config.Unit{
Name: "nginx",
Runtime: "podman",
Enabled: true,
Restart: true,
RestartSec: 10,
}
content, err := Generate(u, "", "")
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
if !strings.Contains(content, "Restart=on-failure") {
t.Errorf("Generate() missing Restart=on-failure:\n%s", content)
}
if !strings.Contains(content, "RestartSec=10") {
t.Errorf("Generate() missing RestartSec=10:\n%s", content)
}
if strings.Contains(content, "StartLimitBurst") {
t.Errorf("Generate() should not contain StartLimitBurst when retries=0:\n%s", content)
}
})
}
func TestGenerate_CustomCommand(t *testing.T) { func TestGenerate_CustomCommand(t *testing.T) {
u := config.Unit{ u := config.Unit{
Name: "custom", Name: "custom",
@@ -101,7 +155,7 @@ func TestGenerate_CustomCommand(t *testing.T) {
Enabled: true, Enabled: true,
} }
content, err := Generate(u, "", "", "unitdore") content, err := Generate(u, "", "")
if err != nil { if err != nil {
t.Fatalf("Generate() error: %v", err) t.Fatalf("Generate() error: %v", err)
} }
@@ -118,16 +172,21 @@ func TestGenerate_DockerRuntime(t *testing.T) {
Enabled: true, Enabled: true,
} }
content, err := Generate(u, "", "", "unitdore") content, err := Generate(u, "", "")
if err != nil { if err != nil {
t.Fatalf("Generate() error: %v", err) t.Fatalf("Generate() error: %v", err)
} }
if !strings.Contains(content, "ExecStart=/usr/bin/docker start redis") { checks := []string{
t.Errorf("Generate() wrong ExecStart for docker:\n%s", content) "ExecStart=/usr/bin/docker start -a redis",
"ExecStop=-/usr/bin/docker stop redis",
"After=network.target docker.service",
"Requires=docker.service",
}
for _, check := range checks {
if !strings.Contains(content, check) {
t.Errorf("Generate() docker runtime missing %q in output:\n%s", check, content)
} }
if !strings.Contains(content, "ExecStop=/usr/bin/docker stop redis") {
t.Errorf("Generate() wrong ExecStop for docker:\n%s", content)
} }
} }
@@ -138,7 +197,7 @@ func TestGenerate_UnknownRuntime(t *testing.T) {
Enabled: true, Enabled: true,
} }
content, err := Generate(u, "", "", "unitdore") content, err := Generate(u, "", "")
if err != nil { if err != nil {
t.Fatalf("Generate() should not error on unknown runtime: %v", err) t.Fatalf("Generate() should not error on unknown runtime: %v", err)
} }
@@ -155,7 +214,7 @@ func TestGenerate_WithPrefixSuffix(t *testing.T) {
Enabled: true, Enabled: true,
} }
content, err := Generate(u, "prod-", "-web", "unitdore") content, err := Generate(u, "prod-", "-web")
if err != nil { if err != nil {
t.Fatalf("Generate() error: %v", err) t.Fatalf("Generate() error: %v", err)
} }
@@ -181,7 +240,7 @@ func TestBuildExecCommands(t *testing.T) {
{ {
name: "docker", name: "docker",
unit: config.Unit{Name: "app", Runtime: "docker"}, unit: config.Unit{Name: "app", Runtime: "docker"},
wantStart: "/usr/bin/docker start app", wantStart: "/usr/bin/docker start -a app",
wantStop: "/usr/bin/docker stop app", wantStop: "/usr/bin/docker stop app",
}, },
{ {
+2 -2
View File
@@ -27,8 +27,8 @@ func UnitPath(u config.Unit, prefix, suffix string) (string, error) {
} }
// Install writes the .service file for a unit and reloads systemd. // Install writes the .service file for a unit and reloads systemd.
func Install(u config.Unit, prefix, suffix, serviceUser string) error { func Install(u config.Unit, prefix, suffix string) error {
content, err := Generate(u, prefix, suffix, serviceUser) content, err := Generate(u, prefix, suffix)
if err != nil { if err != nil {
return err return err
} }