Add prefix/suffix support, short ID fallback, unit docs
- config: add Prefix/Suffix fields to Config struct - systemd: ServiceName/Generate/UnitPath/Install/Uninstall/Enable/Disable/Status all accept prefix+suffix - runtime: fall back to short container ID (12 chars) when container has no name - cmd: active, status, install all thread prefix/suffix from config - systemd/generator_test.go: updated all calls + added TestGenerate_WithPrefixSuffix - docs/generated-units.md: full examples of every unit type + ordering + naming - README: updated config docs, prefix/suffix section, link to docs/
This commit is contained in:
35
README.md
35
README.md
@@ -76,6 +76,9 @@ oldthing podman hein no no — container
|
|||||||
**Location:** `/etc/unitdore/units.yaml`
|
**Location:** `/etc/unitdore/units.yaml`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
prefix: "" # optional: prepended to all service names, e.g. "prod-"
|
||||||
|
suffix: "" # optional: appended to all service names, e.g. "-svc"
|
||||||
|
|
||||||
units:
|
units:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
runtime: podman # podman | docker
|
runtime: podman # podman | docker
|
||||||
@@ -94,7 +97,35 @@ units:
|
|||||||
enabled: true
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generated unit file (example)
|
### Prefix and suffix
|
||||||
|
|
||||||
|
Set `prefix` and/or `suffix` to namespace your service files:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
prefix: prod-
|
||||||
|
suffix: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates: `unitdore-prod-nginx.service`, `unitdore-prod-myapp.service`, etc.
|
||||||
|
Prefix/suffix apply to the **service file name only** — container names are unchanged.
|
||||||
|
|
||||||
|
### Unnamed containers
|
||||||
|
|
||||||
|
If a container has no name, `syncup` uses its short container ID (first 12 chars) as the unit name.
|
||||||
|
You can rename it later with `unitdore edit`.
|
||||||
|
|
||||||
|
## Generated unit files
|
||||||
|
|
||||||
|
See **[docs/generated-units.md](docs/generated-units.md)** for full examples covering:
|
||||||
|
- System units (root)
|
||||||
|
- User units (rootless)
|
||||||
|
- Docker vs Podman
|
||||||
|
- Custom commands
|
||||||
|
- Prefix/suffix naming
|
||||||
|
- Startup ordering
|
||||||
|
- Unnamed containers (short ID fallback)
|
||||||
|
|
||||||
|
Quick example:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# /etc/systemd/system/unitdore-nginx.service
|
# /etc/systemd/system/unitdore-nginx.service
|
||||||
@@ -115,8 +146,6 @@ RestartSec=5
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
For user units (rootless containers), files go to `~/.config/systemd/user/` and target `default.target`.
|
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ func runActive(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||||
|
|
||||||
// Sort by order then name for deterministic startup sequence
|
// Sort by order then name for deterministic startup sequence
|
||||||
units := make([]config.Unit, len(cfg.Units))
|
units := make([]config.Unit, len(cfg.Units))
|
||||||
copy(units, cfg.Units)
|
copy(units, cfg.Units)
|
||||||
@@ -44,12 +46,12 @@ func runActive(cmd *cobra.Command, args []string) error {
|
|||||||
if !u.Enabled {
|
if !u.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !systemd.IsInstalled(u) {
|
if !systemd.IsInstalled(u, prefix, suffix) {
|
||||||
fmt.Printf(" ! %s: not installed — run 'unitdore install' first\n", u.Name)
|
fmt.Printf(" ! %s: not installed — run 'unitdore install' first\n", u.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf(" ▶ enabling: %s...\n", systemd.ServiceName(u))
|
fmt.Printf(" ▶ enabling: %s...\n", systemd.ServiceName(u, prefix, suffix))
|
||||||
if err := systemd.Enable(u); err != nil {
|
if err := systemd.Enable(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 {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||||
installed := 0
|
installed := 0
|
||||||
skipped := 0
|
skipped := 0
|
||||||
removed := 0
|
removed := 0
|
||||||
@@ -38,14 +39,14 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
|||||||
for _, u := range cfg.Units {
|
for _, u := range cfg.Units {
|
||||||
if !u.Enabled {
|
if !u.Enabled {
|
||||||
// Remove unit file if it exists for disabled units
|
// Remove unit file if it exists for disabled units
|
||||||
if systemd.IsInstalled(u) {
|
if systemd.IsInstalled(u, prefix, suffix) {
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Printf(" ~ would remove: %s\n", systemd.ServiceName(u))
|
fmt.Printf(" ~ would remove: %s\n", systemd.ServiceName(u, prefix, suffix))
|
||||||
} else {
|
} else {
|
||||||
if err := systemd.Uninstall(u); err != nil {
|
if err := systemd.Uninstall(u, prefix, suffix); err != nil {
|
||||||
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
|
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" - removed: %s (disabled)\n", systemd.ServiceName(u))
|
fmt.Printf(" - removed: %s (disabled)\n", systemd.ServiceName(u, prefix, suffix))
|
||||||
removed++
|
removed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,21 +57,21 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
content, err := systemd.Generate(u)
|
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
|
||||||
}
|
}
|
||||||
path, _ := systemd.UnitPath(u)
|
path, _ := systemd.UnitPath(u, prefix, suffix)
|
||||||
fmt.Printf("\n--- %s ---\n%s\n", path, content)
|
fmt.Printf("\n--- %s ---\n%s\n", path, content)
|
||||||
installed++
|
installed++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := systemd.Install(u); 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)
|
path, _ := systemd.UnitPath(u, prefix, suffix)
|
||||||
fmt.Printf(" ✓ installed: %s\n", path)
|
fmt.Printf(" ✓ installed: %s\n", path)
|
||||||
installed++
|
installed++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ func runStatus(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||||
|
|
||||||
if len(cfg.Units) == 0 {
|
if len(cfg.Units) == 0 {
|
||||||
fmt.Println("No units configured. Run 'unitdore syncup' to discover containers.")
|
fmt.Println("No units configured. Run 'unitdore syncup' to discover containers.")
|
||||||
return nil
|
return nil
|
||||||
@@ -63,13 +65,13 @@ func runStatus(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
installed := "yes"
|
installed := "yes"
|
||||||
if !systemd.IsInstalled(u) {
|
if !systemd.IsInstalled(u, prefix, suffix) {
|
||||||
installed = "no"
|
installed = "no"
|
||||||
}
|
}
|
||||||
|
|
||||||
state := "—"
|
state := "—"
|
||||||
if systemd.IsInstalled(u) {
|
if systemd.IsInstalled(u, prefix, suffix) {
|
||||||
state = systemd.Status(u)
|
state = systemd.Status(u, prefix, suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows = append(rows, statusRow{
|
rows = append(rows, statusRow{
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ type Unit struct {
|
|||||||
|
|
||||||
// Config is the root config structure.
|
// Config is the root config structure.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Units []Unit `yaml:"units"`
|
Units []Unit `yaml:"units"`
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads and parses the config file at path.
|
// Load reads and parses the config file at path.
|
||||||
|
|||||||
288
docs/generated-units.md
Normal file
288
docs/generated-units.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
# Generated Systemd Unit Files
|
||||||
|
|
||||||
|
Unitdore generates `.service` files from your `units.yaml` config.
|
||||||
|
This document shows every combination with annotated examples.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Unit (root, no user)
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: nginx
|
||||||
|
runtime: podman
|
||||||
|
order: 1
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Written to:** `/etc/systemd/system/unitdore-nginx.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/unitdore-nginx.service
|
||||||
|
# Generated by unitdore — do not edit manually
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Unitdore: nginx (podman)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/podman start -a nginx
|
||||||
|
ExecStop=/usr/bin/podman stop nginx
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `After=network.target` ensures the network is up before starting
|
||||||
|
- `WantedBy=multi-user.target` means it starts in normal boot runlevel
|
||||||
|
- `Restart=on-failure` restarts the container if it exits unexpectedly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Unit (Docker)
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: redis
|
||||||
|
runtime: docker
|
||||||
|
order: 1
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Written to:** `/etc/systemd/system/unitdore-redis.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/unitdore-redis.service
|
||||||
|
# Generated by unitdore — do not edit manually
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Unitdore: redis (docker)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/docker start redis
|
||||||
|
ExecStop=/usr/bin/docker stop redis
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Docker uses `docker start <name>` (no `-a` flag needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Unit (rootless container)
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: myapp
|
||||||
|
runtime: podman
|
||||||
|
user: hein
|
||||||
|
order: 2
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Written to:** `/home/hein/.config/systemd/user/unitdore-myapp.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# ~/.config/systemd/user/unitdore-myapp.service
|
||||||
|
# Generated by unitdore — do not edit manually
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Unitdore: myapp (podman)
|
||||||
|
After=default.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/podman start -a myapp
|
||||||
|
ExecStop=/usr/bin/podman stop myapp
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- User units use `After=default.target` and `WantedBy=default.target`
|
||||||
|
- The file is written to the user's own systemd directory
|
||||||
|
- Unitdore calls `runuser -u hein -- systemctl --user` for enable/start/status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom Command
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: myscript
|
||||||
|
runtime: podman
|
||||||
|
command: /opt/myapp/start.sh
|
||||||
|
order: 3
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Written to:** `/etc/systemd/system/unitdore-myscript.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/unitdore-myscript.service
|
||||||
|
# Generated by unitdore — do not edit manually
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Unitdore: myscript (podman)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/opt/myapp/start.sh
|
||||||
|
ExecStop=
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- When `command` is set, it is used verbatim as `ExecStart`
|
||||||
|
- `ExecStop` is left empty — systemd will send SIGTERM to the process group
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## With Prefix and Suffix
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
prefix: prod-
|
||||||
|
suffix: -svc
|
||||||
|
|
||||||
|
units:
|
||||||
|
- name: nginx
|
||||||
|
runtime: podman
|
||||||
|
order: 1
|
||||||
|
enabled: true
|
||||||
|
- name: api
|
||||||
|
runtime: docker
|
||||||
|
order: 2
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Written to:**
|
||||||
|
- `/etc/systemd/system/unitdore-prod-nginx-svc.service`
|
||||||
|
- `/etc/systemd/system/unitdore-prod-api-svc.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/unitdore-prod-nginx-svc.service
|
||||||
|
# Generated by unitdore — do not edit manually
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Unitdore: nginx (podman)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/podman start -a nginx
|
||||||
|
ExecStop=/usr/bin/podman stop nginx
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `prefix` and `suffix` apply to the **service file name only** — the container name stays unchanged
|
||||||
|
- Useful for distinguishing environments (`prod-`, `staging-`) or roles (`-web`, `-worker`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Container Named by Short ID
|
||||||
|
|
||||||
|
If a container has no name (e.g. started with `docker run` without `--name`), unitdore uses its short container ID (first 12 characters) as the unit name.
|
||||||
|
|
||||||
|
**Example:** Container ID `a3f2c1b9e4d7` with no name →
|
||||||
|
|
||||||
|
**Config entry added by `syncup`:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: a3f2c1b9e4d7
|
||||||
|
runtime: docker
|
||||||
|
order: 1
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Written to:** `/etc/systemd/system/unitdore-a3f2c1b9e4d7.service`
|
||||||
|
|
||||||
|
**Tip:** After `syncup`, use `unitdore edit` to give it a proper name before running `install`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Startup Ordering Example
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: postgres
|
||||||
|
runtime: podman
|
||||||
|
order: 1
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: redis
|
||||||
|
runtime: podman
|
||||||
|
order: 1 # same order = starts in parallel
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: api
|
||||||
|
runtime: podman
|
||||||
|
order: 2 # waits until order 1 group is started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: nginx
|
||||||
|
runtime: podman
|
||||||
|
order: 3
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
`unitdore active` starts units in ascending `order` value, lowest first.
|
||||||
|
Units with the **same order** are started together (parallel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Disabled Unit
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```yaml
|
||||||
|
units:
|
||||||
|
- name: oldthing
|
||||||
|
runtime: podman
|
||||||
|
order: 1
|
||||||
|
enabled: false
|
||||||
|
disabled_reason: container not found
|
||||||
|
```
|
||||||
|
|
||||||
|
- `unitdore install` will **remove** the `.service` file if it exists
|
||||||
|
- `unitdore active` will **skip** disabled units
|
||||||
|
- `unitdore status` shows the reason in the REASON column
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference: 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` |
|
||||||
|
| With both | `/etc/systemd/system/unitdore-prod-<name>-svc.service` |
|
||||||
@@ -12,6 +12,7 @@ type Docker struct{}
|
|||||||
func (d *Docker) Name() string { return "docker" }
|
func (d *Docker) Name() string { return "docker" }
|
||||||
|
|
||||||
type dockerContainer struct {
|
type dockerContainer struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
Names string `json:"Names"`
|
Names string `json:"Names"`
|
||||||
Image string `json:"Image"`
|
Image string `json:"Image"`
|
||||||
Command string `json:"Command"`
|
Command string `json:"Command"`
|
||||||
@@ -41,8 +42,15 @@ func (d *Docker) ListRunning() ([]Container, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.TrimPrefix(c.Names, "/")
|
name := strings.TrimPrefix(c.Names, "/")
|
||||||
|
// Fall back to short container ID if no name assigned
|
||||||
if name == "" {
|
if name == "" {
|
||||||
continue
|
if len(c.ID) >= 12 {
|
||||||
|
name = c.ID[:12]
|
||||||
|
} else if c.ID != "" {
|
||||||
|
name = c.ID
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
containers = append(containers, Container{
|
containers = append(containers, Container{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Podman struct{}
|
|||||||
func (p *Podman) Name() string { return "podman" }
|
func (p *Podman) Name() string { return "podman" }
|
||||||
|
|
||||||
type podmanContainer struct {
|
type podmanContainer struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
Names []string `json:"Names"`
|
Names []string `json:"Names"`
|
||||||
Image string `json:"Image"`
|
Image string `json:"Image"`
|
||||||
Command string `json:"Command"`
|
Command string `json:"Command"`
|
||||||
@@ -41,8 +42,15 @@ func (p *Podman) ListRunning() ([]Container, error) {
|
|||||||
if len(c.Names) > 0 {
|
if len(c.Names) > 0 {
|
||||||
name = c.Names[0]
|
name = c.Names[0]
|
||||||
}
|
}
|
||||||
|
// Fall back to short container ID if no name assigned
|
||||||
if name == "" {
|
if name == "" {
|
||||||
continue
|
if len(c.ID) >= 12 {
|
||||||
|
name = c.ID[:12]
|
||||||
|
} else if c.ID != "" {
|
||||||
|
name = c.ID
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
containers = append(containers, Container{
|
containers = append(containers, Container{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ type templateData struct {
|
|||||||
ExecStop string
|
ExecStop string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceName returns the systemd service name for a unit.
|
// ServiceName returns the systemd service name for a unit, with optional prefix/suffix.
|
||||||
func ServiceName(u config.Unit) string {
|
func ServiceName(u config.Unit, prefix, suffix string) string {
|
||||||
return fmt.Sprintf("unitdore-%s.service", u.Name)
|
return fmt.Sprintf("unitdore-%s%s%s.service", prefix, u.Name, suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate produces the .service file content for a unit.
|
// Generate produces the .service file content for a unit.
|
||||||
func Generate(u config.Unit) (string, error) {
|
func Generate(u config.Unit, prefix, suffix string) (string, error) {
|
||||||
execStart, execStop := buildExecCommands(u)
|
execStart, execStop := buildExecCommands(u)
|
||||||
|
|
||||||
data := templateData{
|
data := templateData{
|
||||||
ServiceName: ServiceName(u),
|
ServiceName: ServiceName(u, prefix, suffix),
|
||||||
Unit: u,
|
Unit: u,
|
||||||
ExecStart: execStart,
|
ExecStart: execStart,
|
||||||
ExecStop: execStop,
|
ExecStop: execStop,
|
||||||
|
|||||||
@@ -9,10 +9,21 @@ import (
|
|||||||
|
|
||||||
func TestServiceName(t *testing.T) {
|
func TestServiceName(t *testing.T) {
|
||||||
u := config.Unit{Name: "nginx"}
|
u := config.Unit{Name: "nginx"}
|
||||||
got := ServiceName(u)
|
|
||||||
want := "unitdore-nginx.service"
|
tests := []struct {
|
||||||
if got != want {
|
prefix, suffix, want string
|
||||||
t.Errorf("ServiceName() = %q, want %q", got, want)
|
}{
|
||||||
|
{"", "", "unitdore-nginx.service"},
|
||||||
|
{"prod-", "", "unitdore-prod-nginx.service"},
|
||||||
|
{"", "-web", "unitdore-nginx-web.service"},
|
||||||
|
{"prod-", "-web", "unitdore-prod-nginx-web.service"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := ServiceName(u, tt.prefix, tt.suffix)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("ServiceName(%q, %q) = %q, want %q", tt.prefix, tt.suffix, got, tt.want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +35,7 @@ func TestGenerate_SystemUnit(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := Generate(u)
|
content, err := Generate(u, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generate() error: %v", err)
|
t.Fatalf("Generate() error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -59,7 +70,7 @@ func TestGenerate_UserUnit(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := Generate(u)
|
content, err := Generate(u, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generate() error: %v", err)
|
t.Fatalf("Generate() error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -77,7 +88,6 @@ func TestGenerate_UserUnit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// System unit markers must NOT appear
|
|
||||||
if strings.Contains(content, "WantedBy=multi-user.target") {
|
if strings.Contains(content, "WantedBy=multi-user.target") {
|
||||||
t.Error("Generate() user unit should not contain multi-user.target")
|
t.Error("Generate() user unit should not contain multi-user.target")
|
||||||
}
|
}
|
||||||
@@ -91,7 +101,7 @@ func TestGenerate_CustomCommand(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := Generate(u)
|
content, err := Generate(u, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generate() error: %v", err)
|
t.Fatalf("Generate() error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -108,7 +118,7 @@ func TestGenerate_DockerRuntime(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := Generate(u)
|
content, err := Generate(u, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generate() error: %v", err)
|
t.Fatalf("Generate() error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -128,7 +138,7 @@ func TestGenerate_UnknownRuntime(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := Generate(u)
|
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)
|
||||||
}
|
}
|
||||||
@@ -138,6 +148,23 @@ func TestGenerate_UnknownRuntime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerate_WithPrefixSuffix(t *testing.T) {
|
||||||
|
u := config.Unit{
|
||||||
|
Name: "nginx",
|
||||||
|
Runtime: "podman",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := Generate(u, "prod-", "-web")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(content, "unitdore-prod-nginx-web.service") {
|
||||||
|
t.Errorf("Generate() prefix+suffix not reflected in service name:\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildExecCommands(t *testing.T) {
|
func TestBuildExecCommands(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -13,26 +13,27 @@ import (
|
|||||||
const systemUnitDir = "/etc/systemd/system"
|
const systemUnitDir = "/etc/systemd/system"
|
||||||
|
|
||||||
// UnitPath returns the filesystem path where the .service file should be written.
|
// UnitPath returns the filesystem path where the .service file should be written.
|
||||||
func UnitPath(u config.Unit) (string, error) {
|
func UnitPath(u config.Unit, prefix, suffix string) (string, error) {
|
||||||
|
svcName := ServiceName(u, prefix, suffix)
|
||||||
if u.User == "" {
|
if u.User == "" {
|
||||||
return filepath.Join(systemUnitDir, ServiceName(u)), nil
|
return filepath.Join(systemUnitDir, svcName), nil
|
||||||
}
|
}
|
||||||
// Rootless: write to the user's systemd config dir
|
// Rootless: write to the user's systemd config dir
|
||||||
home, err := userHomeDir(u.User)
|
home, err := userHomeDir(u.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(home, ".config", "systemd", "user", ServiceName(u)), nil
|
return filepath.Join(home, ".config", "systemd", "user", svcName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) error {
|
func Install(u config.Unit, prefix, suffix string) error {
|
||||||
content, err := Generate(u)
|
content, err := Generate(u, prefix, suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := UnitPath(u)
|
path, err := UnitPath(u, prefix, suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -49,8 +50,8 @@ func Install(u config.Unit) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Uninstall removes the .service file for a unit and reloads systemd.
|
// Uninstall removes the .service file for a unit and reloads systemd.
|
||||||
func Uninstall(u config.Unit) error {
|
func Uninstall(u config.Unit, prefix, suffix string) error {
|
||||||
path, err := UnitPath(u)
|
path, err := UnitPath(u, prefix, suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -63,18 +64,18 @@ func Uninstall(u config.Unit) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable enables and starts a unit.
|
// Enable enables and starts a unit.
|
||||||
func Enable(u config.Unit) error {
|
func Enable(u config.Unit, prefix, suffix string) error {
|
||||||
return systemctl(u.User, "enable", "--now", ServiceName(u))
|
return systemctl(u.User, "enable", "--now", ServiceName(u, prefix, suffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable disables and stops a unit.
|
// Disable disables and stops a unit.
|
||||||
func Disable(u config.Unit) error {
|
func Disable(u config.Unit, prefix, suffix string) error {
|
||||||
return systemctl(u.User, "disable", "--now", ServiceName(u))
|
return systemctl(u.User, "disable", "--now", ServiceName(u, prefix, suffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns the ActiveState of a unit ("active", "inactive", "failed", "unknown").
|
// Status returns the ActiveState of a unit ("active", "inactive", "failed", "unknown").
|
||||||
func Status(u config.Unit) string {
|
func Status(u config.Unit, prefix, suffix string) string {
|
||||||
args := []string{"show", "-p", "ActiveState", "--value", ServiceName(u)}
|
args := []string{"show", "-p", "ActiveState", "--value", ServiceName(u, prefix, suffix)}
|
||||||
var out []byte
|
var out []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -90,8 +91,8 @@ func Status(u config.Unit) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsInstalled returns true if the unit file exists on disk.
|
// IsInstalled returns true if the unit file exists on disk.
|
||||||
func IsInstalled(u config.Unit) bool {
|
func IsInstalled(u config.Unit, prefix, suffix string) bool {
|
||||||
path, err := UnitPath(u)
|
path, err := UnitPath(u, prefix, suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user