295eb22826
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
3.4 KiB
Go
91 lines
3.4 KiB
Go
// 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
|
|
}
|