panel v0.9.2 — open-source game server manager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 01:06:16 -07:00
commit 0f6aea796c
2164 changed files with 301480 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
// Package updater implements the Generic Update system: pluggable
// providers that fetch new versions of game files from SteamCMD,
// GitHub releases, or a direct URL, then lay them down under the
// instance's data path (bind mount) or container volume.
//
// The panel's manifest declares one or more update_providers per module;
// the operator picks one to trigger. Each provider runs on the *Agent*
// (close to the data) and streams progress back as LogLine envelopes
// so the dashboard renders updates in the same events pane as normal
// server output.
package updater
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"github.com/dbledeez/panel/agent/internal/runtime"
modulepkg "github.com/dbledeez/panel/pkg/module"
)
// Context is the environment an update runs in.
type Context struct {
InstanceID string
Manifest *modulepkg.Manifest
ContainerID string // empty if container doesn't exist; present for running + stopped
BrowseableRoot string // container-absolute install root, when applicable
DataPath string // host bind-mount root, fallback
Runtime runtime.Runtime // for container file ops + sidecar launches (future)
Log func(line string) // streams a progress line upstream via LogLine
// Steam login credentials for SteamCMD apps that refuse +login
// anonymous (DayZ, Arma, etc.). Populated by the controller from its
// encrypted steam_credentials store, forwarded via the mTLS-protected
// gRPC stream. The agent redacts SteamPassword from log output before
// it ever hits the bus; see steamcmd.go.
SteamUsername string
SteamPassword string
}
// Provider performs an update according to its kind.
type Provider interface {
Name() string
Update(ctx context.Context, uc *Context, spec *modulepkg.UpdateProvider) error
}
// Get returns the Provider implementation for the given kind, or an
// error if the kind is unknown.
func Get(kind string) (Provider, error) {
switch kind {
case "direct":
return &DirectProvider{}, nil
case "github":
return &GitHubProvider{}, nil
case "steamcmd":
return &SteamCMDProvider{}, nil
default:
return nil, fmt.Errorf("unknown update provider kind %q", kind)
}
}
// writeToInstance puts data at the instance-relative path. Prefers
// container-path ops when the instance has a BrowseableRoot and a live
// container; otherwise falls back to writing under DataPath.
func writeToInstance(ctx context.Context, uc *Context, targetPath string, data []byte) error {
if targetPath == "" {
return fmt.Errorf("target_path is required")
}
if uc.BrowseableRoot != "" && uc.ContainerID != "" {
abs := path.Join(uc.BrowseableRoot, targetPath)
// Ensure parent dir exists inside the container via runtime exec.
// We don't have a helper in the interface; we just try to write
// and rely on CopyFileToContainer returning a clear error if the
// parent is missing. (itzg and most images pre-create /data.)
return uc.Runtime.CopyFileToContainer(ctx, uc.ContainerID, abs, data)
}
if uc.DataPath == "" {
return fmt.Errorf("no storage available: container has no BrowseableRoot and instance has no DataPath")
}
abs := filepath.Join(uc.DataPath, filepath.FromSlash(targetPath))
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
return fmt.Errorf("mkdir %s: %w", filepath.Dir(abs), err)
}
if err := os.WriteFile(abs, data, 0o644); err != nil {
return fmt.Errorf("write %s: %w", abs, err)
}
return nil
}