feat(release): harden production packaging
CI / validate (pull_request) Failing after 2m30s

This commit is contained in:
2026-08-09 22:11:21 +02:00
parent deb108881b
commit 46bc90a0e8
23 changed files with 739 additions and 196 deletions
+7
View File
@@ -0,0 +1,7 @@
# Public deployment settings only. Internal paths and secrets are automatic.
DOGAMA_VERSION=latest
DOGAMA_HTTP_PORT=8080
TZ=UTC
DOGAMA_DATA_PATH=./data
DOGAMA_SERVERS_PATH=./data/servers
DOGAMA_BACKUPS_PATH=./data/backups
+38
View File
@@ -0,0 +1,38 @@
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install specification validators
run: python -m pip install --requirement tools/requirements-validation.txt
- name: Check formatting
run: test -z "$(gofmt -l .)"
- name: Test
run: go test ./...
- name: Build without cgo
run: CGO_ENABLED=0 go build ./...
- name: Race detection
run: go test -race ./...
- name: Vet
run: go vet ./...
- name: Staticcheck
run: go run honnef.co/go/tools/cmd/staticcheck@v0.6.1 ./...
- uses: golangci/golangci-lint-action@v8
with:
version: v2.4.0
- name: Validate specifications and Compose contract
run: python tools/validate_spec.py
+96
View File
@@ -0,0 +1,96 @@
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install --requirement tools/requirements-validation.txt
- name: Validate release tag
run: echo "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'
- name: Check formatting
run: test -z "$(gofmt -l .)"
- run: go test ./...
- run: CGO_ENABLED=0 go build ./...
- run: go test -race ./...
- run: go vet ./...
- run: go run honnef.co/go/tools/cmd/staticcheck@v0.6.1 ./...
- uses: golangci/golangci-lint-action@v8
with:
version: v2.4.0
- run: python tools/validate_spec.py
publish:
needs: validate
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Derive release metadata
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
echo "VERSION=$version" >> "$GITHUB_ENV"
if [[ "$version" == *-* ]]; then echo "PRERELEASE=true" >> "$GITHUB_ENV"; else echo "PRERELEASE=false" >> "$GITHUB_ENV"; fi
- name: Log in to the Gitea registry
shell: bash
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.zaynet.fr --username codex --password-stdin
- name: Publish application image
shell: bash
run: |
tags="--tag git.zaynet.fr/dogama/dogama:$VERSION"
if [[ "$PRERELEASE" == false ]]; then tags="$tags --tag git.zaynet.fr/dogama/dogama:latest"; fi
docker buildx build --platform linux/amd64,linux/arm64 --target dogama --build-arg VERSION="$GITHUB_REF_NAME" --build-arg COMMIT="$GITHUB_SHA" $tags --push .
- name: Publish agent image
shell: bash
run: |
tags="--tag git.zaynet.fr/dogama/dogama-agent:$VERSION"
if [[ "$PRERELEASE" == false ]]; then tags="$tags --tag git.zaynet.fr/dogama/dogama-agent:latest"; fi
docker buildx build --platform linux/amd64,linux/arm64 --target dogama-agent --build-arg VERSION="$GITHUB_REF_NAME" --build-arg COMMIT="$GITHUB_SHA" $tags --push .
- name: Build release archives
run: make release VERSION="$GITHUB_REF_NAME"
- name: Create release notes
shell: bash
run: |
previous="$(git describe --tags --abbrev=0 "$GITHUB_SHA^" 2>/dev/null || true)"
if [[ -n "$previous" ]]; then range="$previous..$GITHUB_SHA"; else range="$GITHUB_SHA"; fi
{
echo "Container images:"
echo "- git.zaynet.fr/dogama/dogama:$VERSION"
echo "- git.zaynet.fr/dogama/dogama-agent:$VERSION"
echo
echo "Changes included in this tag:"
git log --format='- %s (%h)' "$range"
} > release-notes.md
- name: Create Gitea release
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_COMMIT: ${{ github.sha }}
run: python tools/create_gitea_release.py release-notes.md
+10
View File
@@ -0,0 +1,10 @@
version: "2"
# Staticcheck and go vet run as dedicated gates. Keep this aggregate gate
# focused until the repository's pre-existing errcheck debt is addressed.
linters:
default: none
enable:
- govet
- ineffassign
- unused
+4 -1
View File
@@ -12,11 +12,14 @@ RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama ./cmd/dogama && \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama-agent ./cmd/dogama-agent
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama-agent ./cmd/dogama-agent && \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w" -o /out/dogama-init ./cmd/dogama-init
FROM gcr.io/distroless/static-debian12:nonroot@sha256:f5b485ea962d9bd1186b2f6b3a061191539b905b82ec395de78cbfae51f20e35 AS dogama
WORKDIR /var/lib/dogama
COPY --from=build /out/dogama /usr/local/bin/dogama
COPY --from=build /out/dogama-init /usr/local/bin/dogama-init
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/dogama"]
+148 -67
View File
@@ -1,84 +1,165 @@
# DoGaMa
![DoGaMa synthwave banner](docs/assets/dogama-brand-banner.png)
![DoGaMa banner](docs/assets/dogama-brand-banner.png)
DoGaMa is a lightweight, self-hosted manager for private game servers running as Docker containers. It is designed for families and small groups of friends, not for commercial hosting or general Docker administration.
DoGaMa is a lightweight, self-hosted manager for private Docker game servers. It provides a web interface for families and small groups while keeping direct Docker access isolated in a restricted private agent.
This repository contains the deployable DoGaMa V1 implementation and its normative product and engineering contracts.
DoGaMa V1 is feature-complete. Linux is the production target; advanced instance, catalog and backup workflows remain partly API-first.
## Product invariants
## Features
- DoGaMa only displays and operates game-server containers that it created or explicitly adopted through a controlled administrator workflow.
- The main application never mounts the Docker socket. A separate, private, restricted agent is the only component allowed to reach Docker.
- The main application is a small Go service with an embedded web UI and SQLite.
- Game-specific integrations are exclusively lightweight WebAssembly adapters. They never run as privileged host processes or sidecar containers.
- A module may contact only the API endpoint of its assigned instance, through host-provided functions and declared ports.
- Templates are declarative, versioned YAML documents validated against a JSON Schema.
- Almost all operational configuration is performed in the web interface. `compose.yaml` only bootstraps DoGaMa itself.
- Secrets are never returned after submission, logged, audited, or included in normal exports.
- Destructive operations preserve player data and backups by default.
- Local administrator bootstrap, accounts, sessions, roles and per-instance permissions.
- Validated game catalog and deployment previews, with Palworld as the reference integration.
- Controlled create, inspect, start, stop, restart, update and container-only deletion workflows.
- Persistent player data, scheduled and manual backups, safe import, export and restore.
- Digest-aware updates with optional safety backups, readiness checks and rollback.
- Sandboxed WebAssembly game adapters; no native plugins or host scripts.
- Encrypted notification channels, bounded delivery retries and security-focused audit events.
- Responsive server-rendered web interface with no external runtime asset dependency.
## Documentation map
## Screenshots
### Product
The repository currently includes the official banner above but no maintained product screenshots. Screenshots will be added only when they can be kept aligned with released UI behavior.
- [Vision and scope](docs/product/vision-and-scope.md)
- [V1 acceptance criteria](docs/product/acceptance-criteria.md)
- [Roadmap](docs/product/roadmap.md)
## Installation
### Architecture and domain
Install Docker Engine with the Compose plugin on a Linux host. Save the repository's [`compose.yaml`](compose.yaml), optionally copy [`.env.example`](.env.example) to `.env`, then run:
- [System architecture](docs/architecture/system-architecture.md)
- [Main application](docs/architecture/main-application.md)
- [Restricted Docker agent](docs/architecture/docker-agent.md)
- [WebAssembly module runtime](docs/architecture/wasm-modules.md)
- [Data model](docs/domain/data-model.md)
- [Roles and permissions](docs/domain/authorization.md)
- [Instance lifecycle](docs/domain/instance-lifecycle.md)
### Operations and security
- [Backups, import, restore and export](docs/operations/backups-import-export.md)
- [Resources, ports, storage, mods and updates](docs/operations/instance-operations.md)
- [Notifications and audit](docs/operations/notifications-and-audit.md)
- [Deployment and release](docs/operations/deployment-and-release.md)
- [Security and threat model](docs/security/security-and-threat-model.md)
- [Administration and manager interfaces](docs/ux/interfaces.md)
### Contributor contracts
- [Current operational project state](docs/PROJECT-STATE.md)
- [Development conventions](docs/contributing/development.md)
- [Contributor testing](docs/contributing/testing.md)
- [AI and Codex contributor guide](docs/contributing/ai-codex-guide.md)
- [Template schema](specs/template.schema.json)
- [Module manifest schema](specs/module-manifest.schema.json)
- [Normalized module API](specs/normalized-module-api.md)
- [Palworld reference template](catalog/palworld/template.yaml)
- [Palworld reference module manifest](modules/palworld-rest/manifest.yaml)
## Intended deployment
```text
Browser
|
v
DoGaMa main application ---- SQLite / catalog / backups
|
| private authenticated API
v
Restricted Docker agent ---- Docker socket
|
v
Managed game-server containers
```sh
mkdir -p data/servers data/backups
docker compose pull
docker compose up -d
```
Only the main application's HTTP port is published. The agent and game-management APIs remain on private Docker networks. Individual game ports are published by the managed instances according to approved templates and administrator configuration.
The default web address is `http://HOST:8080`. DoGaMa generates its internal agent token and encryption key automatically on first start; do not create or add them to `.env`.
## Status
A complete minimal Compose deployment is:
The V1 roadmap is implemented. See the current operational baseline and known limitations in [PROJECT-STATE.md](docs/PROJECT-STATE.md), and use the deployment guide for production installation and release verification.
```yaml
services:
init:
image: git.zaynet.fr/dogama/dogama:${DOGAMA_VERSION:-latest}
user: "0:0"
entrypoint: ["/usr/local/bin/dogama-init"]
volumes:
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
cap_add: [CHOWN, DAC_READ_SEARCH, FOWNER]
## Validate the specification
dogama:
image: git.zaynet.fr/dogama/dogama:${DOGAMA_VERSION:-latest}
restart: unless-stopped
ports:
- "${DOGAMA_HTTP_PORT:-8080}:8080"
environment:
DOGAMA_AGENT_URL: http://agent:8081
volumes:
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent:ro
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
depends_on:
init:
condition: service_completed_successfully
agent:
condition: service_started
Install the temporary validation dependencies from `tools/requirements-validation.txt`, then run `python tools/validate_spec.py`. The check validates both JSON Schemas, YAML examples, cross-referenced ports/mounts/capabilities, packaged-asset checksums, JSON fixtures, requirement coverage and internal Markdown links.
agent:
image: git.zaynet.fr/dogama/dogama-agent:${DOGAMA_VERSION:-latest}
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- agent_state:/var/lib/dogama-agent
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
depends_on:
init:
condition: service_completed_successfully
volumes:
agent_state:
```
Use the repository Compose file for its complete network and container-hardening settings; the excerpt shows only the installation contract.
## Initial setup
Open `/setup`, create the first administrator, then sign in. Configure global labels and notification channels from Settings. Create game instances only after checking the host paths, ports and backup policy shown by the deployment preview.
## Updating
Pin `DOGAMA_VERSION` to a released version such as `0.1.0`, back up the DoGaMa data directory, then pull and recreate:
```sh
docker compose pull
docker compose up -d
```
Never delete `data`, the server/backup directories or the internal `agent_state` volume during an update.
## Volumes and persistent data
| Host setting | Container path | Contents |
|---|---|---|
| `DOGAMA_DATA_PATH` | `/var/lib/dogama` | SQLite database, imports and the application-only master key |
| `DOGAMA_SERVERS_PATH` | `/srv/game-servers` | Game-server configuration and player data |
| `DOGAMA_BACKUPS_PATH` | `/srv/game-backups` | DoGaMa-managed game backups |
`agent_state` is an internal named volume. It contains the shared authentication token and the authenticated agent registry that prevents operations against unknown containers. It is not a user configuration surface, but it must be backed up with the other DoGaMa state.
## Ports
| Port | Exposure | Purpose |
|---|---|---|
| `8080/tcp` | Host, configurable | DoGaMa web interface and API |
| `8081/tcp` | Private Compose network only | Authenticated application-to-agent API |
Managed game ports are selected per instance from validated templates.
## Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `DOGAMA_VERSION` | `latest` | Image version; pin a release in production |
| `DOGAMA_HTTP_PORT` | `8080` | Published web port |
| `TZ` | `UTC` | Container timezone |
| `DOGAMA_DATA_PATH` | `./data` | Host path for DoGaMa data |
| `DOGAMA_SERVERS_PATH` | `./data/servers` | Host path for game-server data |
| `DOGAMA_BACKUPS_PATH` | `./data/backups` | Host path for backups |
Internal container paths, allowed roots and service authentication are intentionally not configurable through the public Compose interface.
## Security
- The main application never mounts the Docker socket.
- Only the private, non-published agent can access Docker, through typed and deny-by-default operations.
- The application runs non-root with a read-only root filesystem; both long-running services drop Linux capabilities.
- Internal secrets are generated from the operating system cryptographic random source, stored with restrictive permissions and never logged or exposed in the UI.
- The master key is mounted only through the application data path; the agent has no access to it.
- Agent path checks remain fixed to `/srv/game-servers` and `/srv/game-backups` inside the containers.
Use a trusted TLS reverse proxy and restrict access to all host data directories. The Docker agent still has host-equivalent power through the socket and must never be published.
## Supported platforms
Release images and archives target Linux `amd64` and `arm64`. The full validation suite is designed for Linux; native Windows execution is not supported.
## Documentation
- [Deployment and release](docs/operations/deployment-and-release.md)
- [Current project state](docs/PROJECT-STATE.md)
- [Architecture](docs/architecture/system-architecture.md)
- [Restricted Docker agent](docs/architecture/docker-agent.md)
- [Security and threat model](docs/security/security-and-threat-model.md)
- [Contributor development guide](docs/contributing/development.md)
- [Contributor testing guide](docs/contributing/testing.md)
## Support and issues
Report reproducible problems in the [Gitea issue tracker](https://git.zaynet.fr/DoGaMa/DoGaMa-serv/issues). Include the DoGaMa version, host architecture and sanitized logs; never attach secrets, databases or player data.
## License
No repository-wide license file is currently present, so no general redistribution license is asserted here. The Palworld reference module has its own [license](modules/palworld-rest/LICENSE). A project-wide license must be added by the owner before public distribution.
+52
View File
@@ -0,0 +1,52 @@
// Command dogama-init initializes persistent secrets for the standard Compose deployment.
package main
import (
"fmt"
"os"
"strconv"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/internalsecrets"
)
func main() {
uid, gid := integer("DOGAMA_APP_UID", 65532), integer("DOGAMA_APP_GID", 65532)
items := []struct {
path string
uid, gid int
mode os.FileMode
}{
{"/var/lib/dogama-agent/secrets/token", 0, gid, 0o640},
{"/var/lib/dogama/secrets/master_key", uid, gid, 0o600},
}
for _, item := range items {
if _, err := internalsecrets.Ensure(item.path, item.uid, item.gid, item.mode); err != nil {
fmt.Fprintf(os.Stderr, "internal secret initialization failed: %v\n", err)
os.Exit(1)
}
}
for _, path := range []string{"/var/lib/dogama", "/srv/game-servers", "/srv/game-backups"} {
if err := prepareDirectory(path, uid, gid); err != nil {
fmt.Fprintf(os.Stderr, "persistent directory initialization failed: %v\n", err)
os.Exit(1)
}
}
}
func prepareDirectory(path string, uid, gid int) error {
if err := os.MkdirAll(path, 0o750); err != nil {
return err
}
if err := os.Chmod(path, 0o750); err != nil {
return err
}
return os.Chown(path, uid, gid)
}
func integer(name string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(name))
if err != nil || value < 0 {
return fallback
}
return value
}
+13 -5
View File
@@ -21,6 +21,7 @@ import (
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/importexport"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/instance"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/internalsecrets"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/notification"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/persistence/sqlite"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/web"
@@ -65,24 +66,31 @@ func run(logger *slog.Logger) error {
var backupService *backup.Service
auditService := audit.New(db)
var notificationService *notification.Service
if keyFile := os.Getenv("DOGAMA_MASTER_KEY_FILE"); keyFile != "" {
keyFile := environment("DOGAMA_MASTER_KEY_FILE", "secrets/master_key")
if keyFile == "secrets/master_key" {
if _, keyErr := internalsecrets.Ensure(keyFile, os.Getuid(), os.Getgid(), 0o600); keyErr != nil {
return errors.New("initialize encryption key file")
}
}
if keyFile != "" {
key, keyErr := os.ReadFile(keyFile)
if keyErr != nil {
return errors.New("read encryption key file")
}
key = bytes.TrimSpace(key)
notificationService, keyErr = notification.New(db, key)
if keyErr != nil {
return keyErr
}
} else {
logger.Warn("notification channel configuration disabled: DOGAMA_MASTER_KEY_FILE is unset", "event", "notification.disabled")
}
importService, err := importexport.New(repository, environment("DOGAMA_IMPORTS_ROOT", "/var/lib/dogama/imports/staging"), serversRoot)
if err != nil {
return err
}
agentURL, tokenFile := os.Getenv("DOGAMA_AGENT_URL"), os.Getenv("DOGAMA_AGENT_TOKEN_FILE")
agentURL := os.Getenv("DOGAMA_AGENT_URL")
tokenFile := os.Getenv("DOGAMA_AGENT_TOKEN_FILE")
if agentURL != "" && tokenFile == "" {
tokenFile = "/var/lib/dogama-agent/secrets/token"
}
if agentURL == "" && tokenFile == "" {
logger.Warn("instance lifecycle disabled", "event", "lifecycle.disabled")
} else {
+34 -31
View File
@@ -1,6 +1,25 @@
services:
init:
image: git.zaynet.fr/dogama/dogama:${DOGAMA_VERSION:-latest}
user: "0:0"
read_only: true
entrypoint: ["/usr/local/bin/dogama-init"]
volumes:
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_READ_SEARCH
- FOWNER
dogama:
image: ghcr.io/dogama/dogama:${DOGAMA_VERSION:-latest}
image: git.zaynet.fr/dogama/dogama:${DOGAMA_VERSION:-latest}
restart: unless-stopped
read_only: true
ports:
@@ -8,18 +27,11 @@ services:
environment:
TZ: ${TZ:-UTC}
DOGAMA_AGENT_URL: http://agent:8081
DOGAMA_AGENT_TOKEN_FILE: /run/secrets/agent_token
DOGAMA_MASTER_KEY_FILE: /run/secrets/master_key
DOGAMA_SERVERS_ROOT: /srv/game-servers
DOGAMA_BACKUPS_ROOT: /srv/game-backups
DOGAMA_IMPORTS_ROOT: /var/lib/dogama/imports/staging
secrets:
- agent_token
- master_key
volumes:
- ./data:/var/lib/dogama
- ${DOGAMA_SERVERS_ROOT:-/srv/game-servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_ROOT:-/srv/game-backups}:/srv/game-backups
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent:ro
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=32m
security_opt:
@@ -31,28 +43,22 @@ services:
- control
- games
depends_on:
- agent
init:
condition: service_completed_successfully
agent:
condition: service_started
agent:
image: ghcr.io/dogama/dogama-agent:${DOGAMA_VERSION:-latest}
image: git.zaynet.fr/dogama/dogama-agent:${DOGAMA_VERSION:-latest}
restart: unless-stopped
read_only: true
environment:
TZ: ${TZ:-UTC}
DOGAMA_AGENT_LISTEN_ADDRESS: :8081
DOGAMA_AGENT_TOKEN_FILE: /run/secrets/agent_token
DOGAMA_AGENT_REGISTRY_PATH: /var/lib/dogama-agent/registry.json
DOGAMA_DOCKER_SOCKET: /var/run/docker.sock
DOGAMA_DOCKER_NETWORK: dogama-games
DOGAMA_ALLOWED_SERVER_ROOT: /srv/game-servers
DOGAMA_ALLOWED_BACKUP_ROOT: /srv/game-backups
secrets:
- agent_token
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- agent_state:/var/lib/dogama-agent
- ${DOGAMA_SERVERS_ROOT:-/srv/game-servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_ROOT:-/srv/game-backups}:/srv/game-backups
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=16m
security_opt:
@@ -62,6 +68,9 @@ services:
networks:
- control
- games
depends_on:
init:
condition: service_completed_successfully
networks:
frontend:
@@ -72,9 +81,3 @@ networks:
volumes:
agent_state:
secrets:
agent_token:
file: ./secrets/agent_token
master_key:
file: ./secrets/master_key
+6 -3
View File
@@ -4,7 +4,7 @@ Read this compact operational baseline before starting a milestone. Open detaile
## Baseline
- Current reference: milestone 10 working branch from the UI-redesign baseline `cd65414`.
- Current reference: post-milestone-10 production-readiness branch from merged baseline `deb1088`.
- Released SQLite migrations: `0001` through `0009`; never rewrite them.
- Roadmap milestones 1-10 are implemented; this is the V1 feature baseline.
@@ -37,6 +37,8 @@ Read this compact operational baseline before starting a milestone. Open detaile
- Hardened read-only Compose services, capability dropping, private agent networking and distinct minimal OCI image targets.
- Linux black-box bootstrap/authentication E2E coverage plus a documented disposable-Docker V1 release verification matrix.
- Deterministic Linux `amd64`/`arm64` archives with embedded build identity, SPDX module SBOM and SHA-256 checksums.
- Minimal production Compose with automatic persistent internal secrets, fixed container path boundaries and no user-managed application-to-agent settings.
- Gitea CI for pull requests and `main`, plus tag-only multi-architecture image publication and Gitea Release creation.
## Durable decisions
@@ -54,7 +56,8 @@ Read this compact operational baseline before starting a milestone. Open detaile
- The main app never gains Docker-socket access; the agent remains deny-by-default and independently validates privileged plan fields.
- Update candidates are explicit `tag@sha256:digest` references. Mutable tags alone are rejected; automatic updates remain disabled.
- Mod configuration is data-only. Provider commands, scripts and arbitrary download URLs are forbidden.
- Notification configuration is unavailable unless `DOGAMA_MASTER_KEY_FILE` contains exactly 32 bytes; ciphertext is authenticated AES-GCM and secrets are never returned by list APIs.
- The application generates and retains its 32-byte master key outside SQLite; the agent never receives it. Ciphertext is authenticated AES-GCM and secrets are never returned by list APIs.
- `agent_state` remains durable because it stores both the shared token and the MAC-protected instance-to-container registry; losing it must never trigger automatic adoption.
- Notification delivery attempts are capped at five with exponential minute-scale backoff and never determine the originating operation result.
- Audit retention defaults to 30 days and 10,000 entries; zero explicitly selects unlimited retention/count within documented bounds.
@@ -67,7 +70,7 @@ Read this compact operational baseline before starting a milestone. Open detaile
## Validation and CI
- No repository-hosted Gitea/GitHub workflow files are present; release validation is host-agnostic and documented through `Makefile`, `AGENTS.md` and the contributor testing guide.
- `.gitea/workflows/ci.yml` validates pull requests and `main`; `.gitea/workflows/release.yml` publishes only tagged SemVer releases after the same required gate.
- Normal completion gate for Go changes is the validation set in `AGENTS.md` on Linux.
- Specification validation is `python tools/validate_spec.py` with `tools/requirements-validation.txt` available.
- Start with package/file-specific tests, then run global tests, build, race detection, vet, static analysis and schema validation as applicable.
+2 -2
View File
@@ -54,7 +54,7 @@ io.dogama.template-version=<version>
io.dogama.plan-digest=<sha256-of-canonical-plan>
```
Labels alone are insufficient. The agent keeps a durable registry of instance ID, expected container identity and plan digest, authenticated by an agent-local key or MAC. An operation succeeds only when request, registry and inspected labels agree.
Labels alone are insufficient. The agent keeps a durable registry of instance ID, expected container identity and plan digest, authenticated with a key derived from the persistent shared agent token. An operation succeeds only when request, registry and inspected labels agree. This registry is why the internal `agent_state` volume must persist: it cannot be reconstructed safely without adopting containers on labels alone.
## Deployment-plan validation
@@ -83,7 +83,7 @@ V1 templates do not expose arbitrary Docker security options. The agent applies
## Authentication and replay defense
Requests use a shared secret read from a Docker secret file. Sign method, path, body digest, timestamp and nonce. Reject clock-skewed or reused nonces. Use constant-time comparison, small body limits and short timeouts. Rotate the token through an explicit maintenance workflow.
Requests use a shared token generated automatically in the internal `agent_state` volume before both services start. It is owned by `root:65532`, mode `0640`; the application receives the volume read-only and the agent never receives the application master key. Sign method, path, body digest, timestamp and nonce. Reject clock-skewed or reused nonces. Use constant-time comparison, small body limits and short timeouts. Rotate the token through an explicit maintenance workflow.
The wire format is `DoGaMa-HMAC-SHA256 <base64url-signature>` in the
`Authorization` header, with `X-DoGaMa-Timestamp` in RFC 3339 and a random
+2 -2
View File
@@ -59,7 +59,7 @@ Package names express business capabilities. Avoid a generic `utils` package and
## Configuration precedence
1. Bootstrap-only environment or secret files: listen address, database/data root, agent endpoint/token file, master-key file and allowed bind roots.
1. Internal bootstrap contracts: listen address, database/data root, private agent endpoint, automatically generated token and master-key files, and fixed allowed bind roots. The public Compose interface exposes only host-side storage locations.
2. Global administrator settings in SQLite: public game address, defaults, audit retention, notification channels, upload limits and safety policies.
3. Template defaults.
4. Per-instance administrator settings.
@@ -74,4 +74,4 @@ Production CSS and script assets are compiled before the Go build and embedded.
## Secret handling
Encrypt secret values with an authenticated encryption algorithm using a master key external to SQLite. Store key version and nonce with ciphertext. Support key rotation as a maintenance workflow. Decrypt only at the last responsible moment, keep plaintext lifetimes short and redact structured errors.
Encrypt secret values with an authenticated encryption algorithm using a persistent master key stored outside SQLite under the application data path. Generate it atomically at first startup, never replace an existing key, and never mount it into the Docker agent. Store key version and nonce with ciphertext. Support key rotation as a maintenance workflow. Decrypt only at the last responsible moment, keep plaintext lifetimes short and redact structured errors.
+6 -6
View File
@@ -28,7 +28,7 @@ tests/integration/
## Initial application development
The initial main application requires Go 1.25. SQLite is provided by the pure-Go `modernc.org/sqlite` driver, so neither cgo nor a system SQLite development library is required. It reads bootstrap settings from `DOGAMA_LISTEN_ADDRESS` (default `:8080`), `DOGAMA_DATABASE_PATH` (default `dogama.db`), `DOGAMA_AGENT_URL`, `DOGAMA_AGENT_TOKEN_FILE` and `DOGAMA_MASTER_KEY_FILE`. The two agent settings must either both be present or both be absent; lifecycle routes remain disabled when developing without an agent. The master-key file must contain exactly 32 bytes and enables encrypted notification-channel configuration; audit remains available without it. Run it with:
The main application requires Go 1.25. SQLite uses the pure-Go `modernc.org/sqlite` driver, so neither cgo nor a system SQLite development library is required. Standard Compose supplies all internal bootstrap contracts and generates both secrets automatically. Direct developer execution may override `DOGAMA_LISTEN_ADDRESS`, `DOGAMA_DATABASE_PATH`, `DOGAMA_AGENT_URL`, `DOGAMA_AGENT_TOKEN_FILE` and `DOGAMA_MASTER_KEY_FILE`; lifecycle routes remain disabled when both agent overrides are absent. These are development controls, not public deployment settings. Run it with:
```sh
go run ./cmd/dogama
@@ -42,11 +42,11 @@ The restricted agent is a separate binary:
go run ./cmd/dogama-agent
```
It fails closed unless `DOGAMA_AGENT_TOKEN_FILE` references a 32-byte-or-longer
secret and at least one of `DOGAMA_ALLOWED_SERVER_ROOT` or
`DOGAMA_ALLOWED_BACKUP_ROOT` is configured. Its bootstrap-only defaults are
`:8081`, `/var/run/docker.sock`, the fixed `dogama-games` Docker network and
`/var/lib/dogama-agent/registry.json`. The configured roots must already exist
It fails closed unless its token file contains a 32-byte-or-longer secret. The
standard deployment uses `/var/lib/dogama-agent/secrets/token`, confines paths to
`/srv/game-servers` and `/srv/game-backups`, and stores the authenticated
registry at `/var/lib/dogama-agent/registry.json`. Developer overrides remain
available for isolated tests. The configured roots must already exist
and are canonicalized with symlinks resolved. For normal deployment, use the
secret file and private control network defined in `compose.yaml`; never publish
the agent port on the host.
+40 -63
View File
@@ -1,77 +1,54 @@
# Deployment and release
## Production prerequisites
## Production deployment
DoGaMa V1 targets one Linux Docker host with the Compose plugin. Put the public
application behind a trusted TLS reverse proxy; never publish the agent port.
Create the server and backup roots on the host and restrict them to the
administrator responsible for DoGaMa. Back up `data/dogama.db`, the server
roots, and the backup roots using host-level tooling.
Create secrets before the first start. Both files must be readable only by the
deployment administrator; the master key is exactly 32 bytes and the agent
token is at least 32 bytes.
DoGaMa V1 targets one Linux Docker host with the Compose plugin. Use the root `compose.yaml`, place the public HTTP service behind a trusted TLS reverse proxy, and never publish the agent port.
```sh
install -d -m 0700 secrets
install -d -m 0750 -o 65532 -g 65532 data
umask 077
head -c 32 /dev/urandom > secrets/master_key
head -c 32 /dev/urandom > secrets/agent_token
cp .env.example .env
mkdir -p data/servers data/backups
docker compose config --quiet
DOGAMA_VERSION=v1.0.0 docker compose up -d
docker compose pull
docker compose up -d
```
Pin `DOGAMA_VERSION` to an immutable released version in production. The main
application runs as a non-root user with a read-only root filesystem, no Linux
capabilities and no Docker socket. The root-running restricted agent is isolated
on the private control network, has a read-only root filesystem and no added
capabilities; only it receives the Docker socket. The game and backup bind roots
remain writable because lifecycle and recovery workflows require them.
The one-shot `init` service creates the agent token and master encryption key from the operating system cryptographic random source before either long-running service starts. It uses atomic create-without-replacement behavior and restrictive ownership/modes. Repeated starts validate and reuse the existing files; invalid existing files stop initialization instead of generating a replacement. Secret values never enter Compose environment values or logs.
TLS termination must retain DoGaMa's CSP, HSTS, frame, MIME and referrer
headers. Do not make forwarded client addresses authoritative for login rate
limiting. After startup, verify that only the configured application port is
published and that normal use redirects to `/setup` until the first
administrator is created.
The application data path contains SQLite, import staging and the application-only master key. The internal `agent_state` volume contains the agent token and authenticated container-binding registry. The registry is durable security state: losing it makes existing containers unknown rather than silently adopting them. The agent never receives the master key.
The application image uses numeric UID/GID `65532:65532`. Grant that identity
write access to the configured server and backup roots (with ACLs or matching
ownership) while keeping access unavailable to unrelated host users.
Only host-side storage locations, image version, web port and timezone are public Compose settings. Container paths and allowed agent roots are fixed internal contracts. Back up the application data, game servers, backups and `agent_state` volume together.
## Release procedure
From a clean, signed-off release commit, run the complete validation gate in
`AGENTS.md`, then create deterministic Linux archives:
```sh
make release VERSION=v1.0.0
(cd dist/dogama-v1.0.0 && sha256sum -c SHA256SUMS)
```
The release command refuses to overwrite an existing release directory. It
produces static `amd64` and `arm64` archives, embedded Go build information, an
SPDX 2.3 module SBOM and SHA-256 checksums. `SOURCE_DATE_EPOCH` defaults to the
release commit timestamp and may be supplied explicitly for reproduction.
Build the two OCI images from the same commit and version:
```sh
make images VERSION=v1.0.0
```
The Dockerfile has distinct `dogama` and `dogama-agent` targets. Publish both
images under the same immutable version and record their registry digests in
the release notes. A release is complete only after a fresh-host Compose smoke
test, bootstrap, Palworld draft/install against a disposable Docker daemon,
backup/restore, failed-update rollback, and the security-denial checks described
in the contributor testing guide.
The application image uses numeric UID/GID `65532:65532`. Grant that identity write access to the configured application, server and backup paths. The short-lived initializer runs as root only to establish file ownership and retains `CAP_CHOWN`, `CAP_FOWNER` and read/search access needed to validate an existing mode-`0600` key; long-running services keep their existing restricted profiles.
## Upgrade and rollback
Stop the application, take a filesystem-consistent copy of the SQLite database,
then change only `DOGAMA_VERSION` and start Compose. Startup applies append-only
migrations before serving requests. Preserve the pre-upgrade database copy and
all player/backup roots. If startup or validation fails, stop the new containers,
restore the database copy, select the prior image version and start again. Never
roll back only the database while a newer application is writing to it.
No production deployment predates automatic secret initialization, so this change requires no legacy migration. For future updates, stop DoGaMa, take a filesystem-consistent backup of all four persistent stores, change only `DOGAMA_VERSION`, then run `docker compose pull` and `docker compose up -d`.
Startup applies append-only SQLite migrations before serving requests. Never remove or recreate `agent_state`, and never replace an existing master key: doing so would invalidate the agent registry or make encrypted notification data unreadable. On failure, restore all state from the same backup point and select the prior image version.
## Release pipeline
Normal CI runs for pull requests and pushes to `main`; it does not publish images. `.gitea/workflows/release.yml` runs only for tags matching `v*.*.*`, validates the exact tag commit, then publishes the `dogama` and `dogama-agent` images for Linux `amd64` and `arm64`.
The external tag keeps its `v` (`v0.1.0`), while container images use `0.1.0`. Stable releases also update `latest`; prerelease tags containing a hyphen never do. Images are published to:
```text
git.zaynet.fr/dogama/dogama:<version>
git.zaynet.fr/dogama/dogama-agent:<version>
```
The workflow uses the protected Gitea Actions secret `REGISTRY_TOKEN` for registry login and Gitea Release creation. After all validations and both image pushes succeed, it creates `DoGaMa <tag>` against the exact tagged commit with notes derived from actual commit subjects. Tokens are never stored in the repository or printed.
The repository owner should protect the Gitea tag pattern `v*` so only authorized accounts can trigger production publication. Do not create a release tag until the candidate commit has passed the full disposable-host verification below.
## Release verification
Run the complete validation gate in `AGENTS.md`, then verify the release artifacts and both image targets:
```sh
make release VERSION=v0.1.0
(cd dist/dogama-v0.1.0 && sha256sum -c SHA256SUMS)
make images VERSION=v0.1.0
```
On an isolated Docker host, confirm automatic secret initialization and reuse, application bootstrap, Palworld draft/install, lifecycle operations, backup/restore, failed-update rollback, and denial against unrelated containers. Confirm that only the web port is published, the main application lacks the Docker socket, the agent cannot access the master key, and player/backup data survive container recreation and interruption.
+4 -3
View File
@@ -10,9 +10,10 @@ Administrators configure channels entirely in the UI:
Channel secrets are encrypted and write-only. A test action sends a clearly marked test message and reports a redacted result.
The main application reads the 32-byte authenticated-encryption key from
`DOGAMA_MASTER_KEY_FILE`. Without that external key, audit remains available
but channel configuration and delivery are disabled. Generic and Discord
The main application reads its automatically generated 32-byte
authenticated-encryption key from the private application data path. An invalid
or inaccessible existing key stops startup; DoGaMa never silently replaces a
key that protects existing ciphertext. Generic and Discord
webhooks require HTTPS; resolution, redirects and every resolved address reject
loopback, private, link-local, multicast and unspecified networks. Generic
webhooks carry `X-DoGaMa-Event-ID`, `X-DoGaMa-Timestamp` and, when a signing
+2 -2
View File
@@ -14,7 +14,7 @@
- Internet users and authenticated non-admin users may be malicious.
- Managers are trusted only for assigned permissions, not host administration.
- Templates, modules, archives, artwork, webhooks and game responses are untrusted.
- The host administrator controls Compose, secret files and bind roots.
- The host administrator controls Compose and persistent host paths; DoGaMa initializes its internal secret files.
- A fully compromised Docker daemon or host is outside DoGaMa's containment guarantee.
## Principal threats and controls
@@ -41,7 +41,7 @@
V1 local accounts use a current password-hashing algorithm with calibrated parameters. Bootstrap accepts the first administrator only through a one-time local setup state. Sessions rotate at login/privilege change, can be revoked, and never appear in URLs. Critical actions require recent password confirmation.
The deployment documentation must recommend TLS through a trusted reverse proxy and restrictive permissions on `secrets/`, data and backup paths.
The deployment documentation must recommend TLS through a trusted reverse proxy and restrictive permissions on application data, server and backup paths. Internal secret files use restrictive ownership and permissions and are never public Compose inputs.
## Template and module trust
+6 -6
View File
@@ -21,17 +21,17 @@ type Config struct {
// LoadConfig reads the agent's bootstrap settings and shared secret file.
func LoadConfig() (Config, error) {
tokenFile := os.Getenv("DOGAMA_AGENT_TOKEN_FILE")
if tokenFile == "" {
return Config{}, errors.New("DOGAMA_AGENT_TOKEN_FILE is required")
}
tokenFile := environment("DOGAMA_AGENT_TOKEN_FILE", "/var/lib/dogama-agent/secrets/token")
secret, err := readSecretFile(tokenFile)
if err != nil {
return Config{}, err
}
roots := make([]string, 0, 2)
for _, name := range []string{"DOGAMA_ALLOWED_SERVER_ROOT", "DOGAMA_ALLOWED_BACKUP_ROOT"} {
if value := os.Getenv(name); value != "" {
for _, item := range []struct{ name, fallback string }{
{"DOGAMA_ALLOWED_SERVER_ROOT", "/srv/game-servers"},
{"DOGAMA_ALLOWED_BACKUP_ROOT", "/srv/game-backups"},
} {
if value := environment(item.name, item.fallback); value != "" {
roots = append(roots, value)
}
}
+4 -4
View File
@@ -27,16 +27,16 @@ func TestLoadConfigReadsSecretFileAndRoots(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(config.Secret, secret) || len(config.AllowedRoots) != 1 || config.ListenAddress != ":8081" || config.DockerNetwork != "dogama-games" {
if !bytes.Equal(config.Secret, secret) || len(config.AllowedRoots) != 2 || config.ListenAddress != ":8081" || config.DockerNetwork != "dogama-games" {
t.Fatalf("config = %#v", config)
}
}
func TestLoadConfigRequiresTokenAndRoot(t *testing.T) {
t.Setenv("DOGAMA_AGENT_TOKEN_FILE", "")
func TestLoadConfigUsesConfinedProductionDefaults(t *testing.T) {
t.Setenv("DOGAMA_AGENT_TOKEN_FILE", filepath.Join(t.TempDir(), "missing-token"))
t.Setenv("DOGAMA_ALLOWED_SERVER_ROOT", "")
t.Setenv("DOGAMA_ALLOWED_BACKUP_ROOT", "")
if _, err := LoadConfig(); err == nil {
t.Fatal("missing token unexpectedly accepted")
t.Fatal("missing default token unexpectedly accepted")
}
}
+107
View File
@@ -0,0 +1,107 @@
// Package internalsecrets initializes persistent deployment secrets without
// exposing their values through environment variables or logs.
package internalsecrets
import (
"crypto/rand"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
)
const secretSize = 32
// Ensure creates path atomically with cryptographically random bytes. Existing
// destination files are validated and never replaced.
func Ensure(path string, uid, gid int, mode os.FileMode) (bool, error) {
if mode.Perm()&0o007 != 0 || mode.Perm()&0o700 == 0 {
return false, errors.New("secret permissions are invalid")
}
if err := validateExisting(path, mode); err == nil {
return false, nil
} else if !errors.Is(err, os.ErrNotExist) {
return false, err
}
value := make([]byte, secretSize)
if _, err := io.ReadFull(rand.Reader, value); err != nil {
return false, fmt.Errorf("generate secret: %w", err)
}
directory := filepath.Dir(path)
directoryMode := os.FileMode(0o700)
if mode.Perm()&0o040 != 0 {
directoryMode = 0o750
}
if err := os.MkdirAll(directory, directoryMode); err != nil {
return false, fmt.Errorf("create secret directory: %w", err)
}
if err := os.Chmod(directory, directoryMode); err != nil {
return false, fmt.Errorf("restrict secret directory: %w", err)
}
temporary, err := os.CreateTemp(directory, ".secret-*")
if err != nil {
return false, fmt.Errorf("create temporary secret: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(mode); err != nil {
temporary.Close()
return false, fmt.Errorf("restrict temporary secret: %w", err)
}
if _, err := temporary.Write(value); err != nil {
temporary.Close()
return false, fmt.Errorf("write temporary secret: %w", err)
}
if err := temporary.Sync(); err != nil {
temporary.Close()
return false, fmt.Errorf("sync temporary secret: %w", err)
}
if err := temporary.Close(); err != nil {
return false, fmt.Errorf("close temporary secret: %w", err)
}
if err := os.Link(temporaryPath, path); err != nil {
if errors.Is(err, os.ErrExist) {
return false, validateExisting(path, mode)
}
return false, fmt.Errorf("install secret: %w", err)
}
if runtime.GOOS != "windows" && os.Geteuid() == 0 {
if err := os.Chown(path, uid, gid); err != nil {
return false, fmt.Errorf("set secret owner: %w", err)
}
if err := os.Chown(directory, uid, gid); err != nil {
return false, fmt.Errorf("set secret directory owner: %w", err)
}
}
return true, nil
}
func validateExisting(path string, mode os.FileMode) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("secret must be a regular file")
}
if runtime.GOOS != "windows" && info.Mode().Perm() != mode.Perm() {
return errors.New("secret permissions do not match the required mode")
}
_, err = readSecret(path)
return err
}
func readSecret(path string) ([]byte, error) {
value, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if len(value) != secretSize {
return nil, fmt.Errorf("secret must contain exactly %d bytes", secretSize)
}
return value, nil
}
+83
View File
@@ -0,0 +1,83 @@
package internalsecrets
import (
"bytes"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
)
func TestEnsureGeneratesAndReusesSecret(t *testing.T) {
path := filepath.Join(t.TempDir(), "private", "token")
created, err := Ensure(path, os.Getuid(), os.Getgid(), 0o600)
if err != nil || !created {
t.Fatalf("first Ensure() = %v, %v", created, err)
}
first, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
created, err = Ensure(path, os.Getuid(), os.Getgid(), 0o600)
if err != nil || created {
t.Fatalf("second Ensure() = %v, %v", created, err)
}
second, _ := os.ReadFile(path)
if len(first) != 32 || !bytes.Equal(first, second) {
t.Fatal("secret was not generated once and reused")
}
if runtime.GOOS != "windows" {
info, _ := os.Stat(path)
if info.Mode().Perm() != 0o600 {
t.Fatalf("secret permissions = %o", info.Mode().Perm())
}
}
}
func TestEnsureConcurrentInitializationCreatesOneSecret(t *testing.T) {
path := filepath.Join(t.TempDir(), "private", "token")
var wait sync.WaitGroup
created := make(chan bool, 2)
errors := make(chan error, 2)
for range 2 {
wait.Add(1)
go func() {
defer wait.Done()
wasCreated, err := Ensure(path, os.Getuid(), os.Getgid(), 0o600)
created <- wasCreated
errors <- err
}()
}
wait.Wait()
close(created)
close(errors)
createdCount := 0
for wasCreated := range created {
if wasCreated {
createdCount++
}
}
for err := range errors {
if err != nil {
t.Fatal(err)
}
}
if createdCount != 1 {
t.Fatalf("created count = %d, want 1", createdCount)
}
}
func TestEnsureRefusesInvalidExistingSecret(t *testing.T) {
path := filepath.Join(t.TempDir(), "master_key")
if err := os.WriteFile(path, []byte("short"), 0o600); err != nil {
t.Fatal(err)
}
if created, err := Ensure(path, os.Getuid(), os.Getgid(), 0o600); err == nil || created {
t.Fatalf("invalid existing secret was accepted: %v, %v", created, err)
}
got, _ := os.ReadFile(path)
if string(got) != "short" {
t.Fatal("invalid existing secret was replaced")
}
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Create the Gitea release for the current, already validated tag."""
import json
import os
import sys
import urllib.request
from pathlib import Path
def main() -> int:
notes = Path(sys.argv[1]).read_text(encoding="utf-8")
tag = os.environ["RELEASE_TAG"]
payload = json.dumps({
"tag_name": tag,
"target_commitish": os.environ["RELEASE_COMMIT"],
"name": f"DoGaMa {tag}",
"body": notes,
"draft": False,
"prerelease": "-" in tag,
}).encode()
url = f'{os.environ["GITEA_SERVER_URL"]}/api/v1/repos/{os.environ["GITEA_REPOSITORY"]}/releases'
request = urllib.request.Request(url, data=payload, method="POST", headers={
"Authorization": f'token {os.environ["GITEA_TOKEN"]}',
"Content-Type": "application/json",
})
with urllib.request.urlopen(request, timeout=30) as response:
if response.status != 201:
raise RuntimeError(f"Gitea release creation returned HTTP {response.status}")
print(f"Created Gitea release {tag}.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+2
View File
@@ -23,6 +23,8 @@ for arch in amd64 arm64; do
-ldflags="-s -w -X main.version=$version -X main.commit=$commit" -o "$stage/dogama" ./cmd/dogama
CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$version -X main.commit=$commit" -o "$stage/dogama-agent" ./cmd/dogama-agent
CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags="-s -w" -o "$stage/dogama-init" ./cmd/dogama-init
go version -m "$stage/dogama" > "$stage/build-info.txt"
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@$epoch" \
-C "$release_dir" -czf "$release_dir/dogama-${version}-linux-$arch.tar.gz" "linux-$arch"
+38 -1
View File
@@ -114,10 +114,47 @@ def validate_coverage() -> None:
raise ValueError("Missing required coverage: " + ", ".join(missing))
def validate_compose() -> None:
compose = load_yaml(ROOT / "compose.yaml")
services = compose["services"]
assert set(services) == {"init", "dogama", "agent"}, "Compose must expose only the initializer and required services"
forbidden = {
"DOGAMA_AGENT_TOKEN_FILE", "DOGAMA_MASTER_KEY_FILE", "DOGAMA_SERVERS_ROOT",
"DOGAMA_BACKUPS_ROOT", "DOGAMA_IMPORTS_ROOT", "DOGAMA_ALLOWED_SERVER_ROOT",
"DOGAMA_ALLOWED_BACKUP_ROOT",
}
for name, service in services.items():
environment = service.get("environment", {})
assert forbidden.isdisjoint(environment), f"{name} exposes an internal environment setting"
assert "secrets" not in service, f"{name} still requires a user-provided Compose secret"
assert "secrets" not in compose, "Compose still defines user-provided internal secrets"
assert "/var/run/docker.sock:/var/run/docker.sock" not in services["dogama"].get("volumes", []), "main application mounts Docker"
assert not services["agent"].get("ports"), "agent must not publish a port"
assert "agent_state:/var/lib/dogama-agent" in services["agent"]["volumes"], "authenticated agent registry is not persistent"
assert "agent_state:/var/lib/dogama-agent:ro" in services["dogama"]["volumes"], "main application cannot read the shared token"
assert all("master_key" not in volume for volume in services["agent"]["volumes"]), "agent can access the master key"
rendered = (ROOT / "compose.yaml").read_text(encoding="utf-8")
assert "${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers" in rendered
assert "${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups" in rendered
def validate_workflows() -> None:
ci = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8")
release = (ROOT / ".gitea/workflows/release.yml").read_text(encoding="utf-8")
assert "pull_request:" in ci and "branches: [main]" in ci, "normal CI does not cover PRs and main"
assert "docker login" not in ci and "--push" not in ci, "normal CI can publish images"
assert 'tags:\n - "v*.*.*"' in release, "release workflow tag trigger is incorrect"
assert "needs: validate" in release, "release publication does not depend on validation"
assert release.count("--push .") == 2, "release workflow must publish exactly two image targets"
assert "create_gitea_release.py" in release, "release workflow does not create a Gitea Release"
assert "branches: [main]" not in release and "pull_request:" not in release, "release workflow has a non-tag trigger"
def main() -> int:
template = validate(ROOT / "specs/template.schema.json", ROOT / "catalog/palworld/template.yaml")
manifest = validate(ROOT / "specs/module-manifest.schema.json", ROOT / "modules/palworld-rest/manifest.yaml")
load_yaml(ROOT / "compose.yaml")
validate_compose()
validate_workflows()
for fixture in ROOT.rglob("*.json"):
load_json(fixture)
validate_links()