feat(backups): 主人和管理员可下载单个备份、导出停服世界,字节经一次性票据从导出 Job 流式转给浏览器,备份按 sha256 核对
This commit is contained in:
35 files changed
+4018
-70
No files matched your search
@@ -0,0 +1,125 @@
|
||||
// Package worldexport is the executor behind the world export routes (POST
|
||||
// /servers/{name}/world/export and POST /servers/{name}/backups/{id}/export): a
|
||||
// one-shot Job in the minecraft namespace that reads a stopped server's world,
|
||||
// or one of its stored archives, and PUTs the tar.gz to felis-api's internal
|
||||
// face, which streams it on to the owner's browser as it arrives
|
||||
// (internal/api/exports.go). Nothing is staged on the way: felis-api never
|
||||
// mounts a world or the backup store, and the archive never lands on a disk it
|
||||
// owns.
|
||||
//
|
||||
// Trust model as in internal/restore: the Pod runs under the weak felis-restore
|
||||
// SA with no API token, mounts one volume read-only, and holds no database URL
|
||||
// or Secret. The only credential it gets is the one-time token of this one
|
||||
// upload. felis-api makes every authorization decision before the Job exists.
|
||||
package worldexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"felis.lolicon.best/internal/naming"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// Request is one export as felis-api admitted it.
|
||||
type Request struct {
|
||||
Server string
|
||||
Mode string // ModeWorld or ModeBackup
|
||||
// BackupRef is the archive a ModeBackup export reads.
|
||||
BackupRef string
|
||||
// ID names the export (JobName) and TargetURL/Token are where and how the
|
||||
// Pod hands the archive over.
|
||||
ID string
|
||||
TargetURL string
|
||||
Token string
|
||||
}
|
||||
|
||||
// Config parameterises the executor. Image and BackupPVC have no safe default:
|
||||
// cmd/felis leaves the API's Exporter nil when either is missing, and the
|
||||
// routes answer 503.
|
||||
type Config struct {
|
||||
Namespace string
|
||||
ServiceAccount string
|
||||
Image string
|
||||
BackupPVC string
|
||||
// BackupRoot MUST be the path the archives were written under
|
||||
// (cfg.Archive.LocalPath): the stored refs are absolute paths.
|
||||
BackupRoot string
|
||||
WorldsRoot string
|
||||
// Deadline caps the Pod's wall-clock. The archive moves at the browser's
|
||||
// pace, so it is longer than a backup's, and it is also the longest a world
|
||||
// export can keep the server from starting.
|
||||
Deadline time.Duration
|
||||
CPULimit string
|
||||
MemLimit string
|
||||
RunAsUser int64
|
||||
RunAsGroup int64
|
||||
FSGroup int64
|
||||
TTLAfterFinished time.Duration
|
||||
}
|
||||
|
||||
const (
|
||||
defaultNamespace = "minecraft"
|
||||
defaultServiceAccount = "felis-restore"
|
||||
defaultBackupRoot = "/backups"
|
||||
defaultWorldsRoot = "/world"
|
||||
defaultDeadline = 2 * time.Hour
|
||||
defaultCPULimit = "1"
|
||||
defaultMemLimit = "256Mi"
|
||||
defaultTTL = 10 * time.Minute
|
||||
)
|
||||
|
||||
func (c Config) withDefaults() Config {
|
||||
if c.Namespace == "" {
|
||||
c.Namespace = defaultNamespace
|
||||
}
|
||||
if c.ServiceAccount == "" {
|
||||
c.ServiceAccount = defaultServiceAccount
|
||||
}
|
||||
if c.BackupRoot == "" {
|
||||
c.BackupRoot = defaultBackupRoot
|
||||
}
|
||||
if c.WorldsRoot == "" {
|
||||
c.WorldsRoot = defaultWorldsRoot
|
||||
}
|
||||
if c.Deadline <= 0 {
|
||||
c.Deadline = defaultDeadline
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Exporter is the production internal/api.Exporter. It creates the Job with
|
||||
// jobs:create, which felis-api already holds in the minecraft namespace.
|
||||
type Exporter struct {
|
||||
cs kubernetes.Interface
|
||||
cfg Config
|
||||
}
|
||||
|
||||
// New builds an Exporter over the typed clientset.
|
||||
func New(cs kubernetes.Interface, cfg Config) *Exporter {
|
||||
return &Exporter{cs: cs, cfg: cfg.withDefaults()}
|
||||
}
|
||||
|
||||
// Start creates the export Job for r and returns its name.
|
||||
func (e *Exporter) Start(ctx context.Context, r Request) (string, error) {
|
||||
c := e.cfg
|
||||
job, err := ExportJob(JobParams{
|
||||
Server: r.Server, ID: r.ID, Mode: r.Mode,
|
||||
WorldPVC: naming.WorldPVCName(r.Server), BackupPVC: c.BackupPVC, BackupRef: r.BackupRef,
|
||||
TargetURL: r.TargetURL, Token: r.Token,
|
||||
Namespace: c.Namespace, ServiceAccount: c.ServiceAccount, Image: c.Image,
|
||||
BackupRoot: c.BackupRoot, WorldsRoot: c.WorldsRoot, Deadline: c.Deadline,
|
||||
CPULimit: c.CPULimit, MemLimit: c.MemLimit,
|
||||
RunAsUser: c.RunAsUser, RunAsGroup: c.RunAsGroup, FSGroup: c.FSGroup,
|
||||
TTLAfterFinished: c.TTLAfterFinished,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := e.cs.BatchV1().Jobs(c.Namespace).Create(ctx, job, metav1.CreateOptions{}); err != nil {
|
||||
return "", fmt.Errorf("worldexport: create export job: %w", err)
|
||||
}
|
||||
return job.Name, nil
|
||||
}
|
||||
Reference in new issue
Block a user