feat(platform): registry/uploads/backup PVC 容量可配置
This commit is contained in:
6 files changed
+154
-5
No files matched your search
+14
-2
@@ -49,6 +49,9 @@ func cmdManifests(args []string, stdout, stderr io.Writer) int {
|
|||||||
backupPVC := fs.String("backup-pvc", "felis-backups", "name of the world-archive PVC this bundle renders in the Minecraft namespace and advertises to the backup/restore executors via FELIS_BACKUP_PVC (default: felis-backups; pass an empty value to render none, leaving backup/restore answering 503)")
|
backupPVC := fs.String("backup-pvc", "felis-backups", "name of the world-archive PVC this bundle renders in the Minecraft namespace and advertises to the backup/restore executors via FELIS_BACKUP_PVC (default: felis-backups; pass an empty value to render none, leaving backup/restore answering 503)")
|
||||||
worldsHostPath := fs.String("worlds-host-path", "", "node directory the reaper reads worlds from: each world PVC resolves as <path>/<pvc>, or as the stock local-path directory <path>/<pv-name>_<ns>_<pvc-name> (k3s storage root: /var/lib/rancher/k3s/storage); enables the reaper CronJob (requires --archive-local-path and a non-empty --backup-pvc)")
|
worldsHostPath := fs.String("worlds-host-path", "", "node directory the reaper reads worlds from: each world PVC resolves as <path>/<pvc>, or as the stock local-path directory <path>/<pv-name>_<ns>_<pvc-name> (k3s storage root: /var/lib/rancher/k3s/storage); enables the reaper CronJob (requires --archive-local-path and a non-empty --backup-pvc)")
|
||||||
archiveLocalPath := fs.String("archive-local-path", "", "path the backup PVC is mounted at in the reaper CronJob; MUST equal felis.toml [archive] local_path")
|
archiveLocalPath := fs.String("archive-local-path", "", "path the backup PVC is mounted at in the reaper CronJob; MUST equal felis.toml [archive] local_path")
|
||||||
|
registryStorage := fs.String("registry-storage", "", "capacity the registry PVC requests (default 10Gi; k3s local-path does not enforce it)")
|
||||||
|
uploadsStorage := fs.String("uploads-storage", "", "capacity the uploads PVC requests (default 5Gi; k3s local-path does not enforce it)")
|
||||||
|
backupStorage := fs.String("backup-storage", "", "capacity the world-archive PVC requests (default 10Gi; k3s local-path does not enforce it)")
|
||||||
reaperNode := fs.String("reaper-node", "", "node that holds --worlds-host-path: pins the reaper CronJob's pod there via nodeSelector kubernetes.io/hostname (multi-node clusters need this, or the reaper may schedule where the hostPath is empty)")
|
reaperNode := fs.String("reaper-node", "", "node that holds --worlds-host-path: pins the reaper CronJob's pod there via nodeSelector kubernetes.io/hostname (multi-node clusters need this, or the reaper may schedule where the hostPath is empty)")
|
||||||
var velocityCIDRs multiFlag
|
var velocityCIDRs multiFlag
|
||||||
fs.Var(&velocityCIDRs, "velocity-cidr", "CIDR of a Velocity proxy host allowed to reach game port 25565 (repeatable, REQUIRED)")
|
fs.Var(&velocityCIDRs, "velocity-cidr", "CIDR of a Velocity proxy host allowed to reach game port 25565 (repeatable, REQUIRED)")
|
||||||
@@ -139,7 +142,7 @@ func cmdManifests(args []string, stdout, stderr io.Writer) int {
|
|||||||
"(pass --worlds-host-path and --archive-local-path — the archive PVC defaults to felis-backups — to enable it)")
|
"(pass --worlds-host-path and --archive-local-path — the archive PVC defaults to felis-backups — to enable it)")
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := platform.RenderYAML(platform.Params{
|
params := platform.Params{
|
||||||
ControlNamespace: *controlNS,
|
ControlNamespace: *controlNS,
|
||||||
MinecraftNamespace: *minecraftNS,
|
MinecraftNamespace: *minecraftNS,
|
||||||
BuildNamespace: *buildNS,
|
BuildNamespace: *buildNS,
|
||||||
@@ -157,7 +160,16 @@ func cmdManifests(args []string, stdout, stderr io.Writer) int {
|
|||||||
|
|
||||||
ServerEgressDenyCIDRs: []string(serverDenyCIDRs),
|
ServerEgressDenyCIDRs: []string(serverDenyCIDRs),
|
||||||
ServerEgressAllowCIDRs: []string(serverAllowCIDRs),
|
ServerEgressAllowCIDRs: []string(serverAllowCIDRs),
|
||||||
})
|
|
||||||
|
RegistryStorage: *registryStorage,
|
||||||
|
UploadsStorage: *uploadsStorage,
|
||||||
|
BackupStorage: *backupStorage,
|
||||||
|
}
|
||||||
|
if err := params.Validate(); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "felis manifests: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
out, err := platform.RenderYAML(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "felis manifests: render: %v\n", err)
|
fmt.Fprintf(stderr, "felis manifests: render: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -219,3 +219,27 @@ func TestManifestsReaperNodePin(t *testing.T) {
|
|||||||
t.Errorf("--reaper-node without --worlds-host-path: exit = %d, want 2", code)
|
t.Errorf("--reaper-node without --worlds-host-path: exit = %d, want 2", code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestManifestsStorageSizes proves the PVC size flags reach the rendered claims
|
||||||
|
// and a size the API server would reject fails before anything is applied.
|
||||||
|
func TestManifestsStorageSizes(t *testing.T) {
|
||||||
|
var out, errBuf bytes.Buffer
|
||||||
|
code := run([]string{"manifests", "--felis-image", "reg/felis:test", "--velocity-cidr", "10.0.0.5/32",
|
||||||
|
"--registry-storage", "40Gi", "--uploads-storage", "8Gi"}, &out, &errBuf)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit code = %d, stderr = %s", code, errBuf.String())
|
||||||
|
}
|
||||||
|
for _, want := range []string{"storage: 40Gi", "storage: 8Gi"} {
|
||||||
|
if !strings.Contains(out.String(), want) {
|
||||||
|
t.Errorf("bundle lacks %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Reset()
|
||||||
|
errBuf.Reset()
|
||||||
|
code = run([]string{"manifests", "--felis-image", "reg/felis:test", "--velocity-cidr", "10.0.0.5/32",
|
||||||
|
"--registry-storage", "lots"}, &out, &errBuf)
|
||||||
|
if code == 0 || !strings.Contains(errBuf.String(), "registry storage") {
|
||||||
|
t.Errorf("--registry-storage lots: exit %d, stderr %q; want a refusal naming the flag", code, errBuf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,10 @@
|
|||||||
# worlds idle beyond the retention window, and grants the reaper's
|
# worlds idle beyond the retention window, and grants the reaper's
|
||||||
# uid (1000) traverse access to that root — k3s ships it 0700
|
# uid (1000) traverse access to that root — k3s ships it 0700
|
||||||
# root:root (default: unset = no reaper)
|
# root:root (default: unset = no reaper)
|
||||||
|
# FELIS_REGISTRY_STORAGE / FELIS_UPLOADS_STORAGE / FELIS_BACKUP_STORAGE capacity the
|
||||||
|
# registry, uploads and world-archive PVCs request on first install
|
||||||
|
# (defaults: 10Gi, 5Gi, 10Gi). An existing claim keeps its size; on
|
||||||
|
# k3s local-path the number is not enforced, see troubleshooting §9
|
||||||
# PKG_LOCK_TIMEOUT seconds to wait for package-manager locks (default: 900)
|
# PKG_LOCK_TIMEOUT seconds to wait for package-manager locks (default: 900)
|
||||||
# APT_LOCK_TIMEOUT legacy alias for PKG_LOCK_TIMEOUT
|
# APT_LOCK_TIMEOUT legacy alias for PKG_LOCK_TIMEOUT
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
@@ -134,6 +138,10 @@ FELIS_PANEL_NODEPORT="${FELIS_PANEL_NODEPORT:-30443}"
|
|||||||
# construction — a mismatch would leave tarLocal's absolute archive refs unresolvable.
|
# construction — a mismatch would leave tarLocal's absolute archive refs unresolvable.
|
||||||
FELIS_BACKUP_PVC="${FELIS_BACKUP_PVC:-felis-backups}"
|
FELIS_BACKUP_PVC="${FELIS_BACKUP_PVC:-felis-backups}"
|
||||||
FELIS_ARCHIVE_LOCAL_PATH="${FELIS_ARCHIVE_LOCAL_PATH:-/var/lib/felis/archives}"
|
FELIS_ARCHIVE_LOCAL_PATH="${FELIS_ARCHIVE_LOCAL_PATH:-/var/lib/felis/archives}"
|
||||||
|
# PVC capacities; empty keeps `felis manifests`' defaults.
|
||||||
|
FELIS_REGISTRY_STORAGE="${FELIS_REGISTRY_STORAGE:-}"
|
||||||
|
FELIS_UPLOADS_STORAGE="${FELIS_UPLOADS_STORAGE:-}"
|
||||||
|
FELIS_BACKUP_STORAGE="${FELIS_BACKUP_STORAGE:-}"
|
||||||
# Retention is opt-in because it DELETES worlds (after a verified archive): point this at the
|
# Retention is opt-in because it DELETES worlds (after a verified archive): point this at the
|
||||||
# node directory the world volumes live under. On the k3s this installer provisions that is
|
# node directory the world volumes live under. On the k3s this installer provisions that is
|
||||||
# /var/lib/rancher/k3s/storage — the reaper resolves each PVC's local-path directory exactly
|
# /var/lib/rancher/k3s/storage — the reaper resolves each PVC's local-path directory exactly
|
||||||
@@ -2828,6 +2836,15 @@ deploy_bundle() {
|
|||||||
fi
|
fi
|
||||||
manifest_args+=(--worlds-host-path "$FELIS_WORLDS_HOST_PATH" --archive-local-path "$FELIS_ARCHIVE_LOCAL_PATH")
|
manifest_args+=(--worlds-host-path "$FELIS_WORLDS_HOST_PATH" --archive-local-path "$FELIS_ARCHIVE_LOCAL_PATH")
|
||||||
fi
|
fi
|
||||||
|
local size
|
||||||
|
size="$(pvc_size "$CONTROL_NS" registry "$FELIS_REGISTRY_STORAGE" FELIS_REGISTRY_STORAGE)"
|
||||||
|
if [ -n "$size" ]; then manifest_args+=(--registry-storage "$size"); fi
|
||||||
|
size="$(pvc_size "$CONTROL_NS" felis-uploads "$FELIS_UPLOADS_STORAGE" FELIS_UPLOADS_STORAGE)"
|
||||||
|
if [ -n "$size" ]; then manifest_args+=(--uploads-storage "$size"); fi
|
||||||
|
if [ -n "$FELIS_BACKUP_PVC" ]; then
|
||||||
|
size="$(pvc_size "$MINECRAFT_NS" "$FELIS_BACKUP_PVC" "$FELIS_BACKUP_STORAGE" FELIS_BACKUP_STORAGE)"
|
||||||
|
if [ -n "$size" ]; then manifest_args+=(--backup-storage "$size"); fi
|
||||||
|
fi
|
||||||
"$HOST_BIN" manifests "${manifest_args[@]}" | kube apply -f -
|
"$HOST_BIN" manifests "${manifest_args[@]}" | kube apply -f -
|
||||||
restart_existing_control_plane "$had_api" "$had_operator"
|
restart_existing_control_plane "$had_api" "$had_operator"
|
||||||
|
|
||||||
@@ -2841,6 +2858,24 @@ deploy_bundle() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pvc_size <namespace> <claim> <wanted> <env name> prints the size to render the claim
|
||||||
|
# with: its current request when it exists, else the wanted size (empty = the renderer's
|
||||||
|
# default). A claim's request can only grow, and only on a storage class that allows
|
||||||
|
# expansion (k3s local-path does not), so re-applying a different size would fail the
|
||||||
|
# whole apply; a mismatch is reported and left to the operator.
|
||||||
|
pvc_size() {
|
||||||
|
local ns="$1" claim="$2" want="$3" env="$4" have
|
||||||
|
have="$(kube -n "$ns" get pvc "$claim" -o jsonpath='{.spec.resources.requests.storage}' 2>/dev/null || true)"
|
||||||
|
if [ -z "$have" ]; then
|
||||||
|
printf '%s' "$want"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
||||||
|
warn "PVC ${ns}/${claim} already requests ${have}; keeping it (${env}=${want} applies to a new claim; grow this one with kubectl patch where its storage class allows expansion)"
|
||||||
|
fi
|
||||||
|
printf '%s' "$have"
|
||||||
|
}
|
||||||
|
|
||||||
restart_existing_control_plane() {
|
restart_existing_control_plane() {
|
||||||
local had_api="$1" had_operator="$2"
|
local had_api="$1" had_operator="$2"
|
||||||
[ "$had_api$had_operator" != "00" ] || return 0
|
[ "$had_api$had_operator" != "00" ] || return 0
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
)
|
||||||
|
|
||||||
// Identity constants and the Params that parameterise the install bundle.
|
// Identity constants and the Params that parameterise the install bundle.
|
||||||
//
|
//
|
||||||
// The label and SA-name constants are PINNED here because three independent
|
// The label and SA-name constants are PINNED here because three independent
|
||||||
@@ -171,6 +177,35 @@ type Params struct {
|
|||||||
// must-match. It is reaper-only and has no default; empty (with WorldsHostPath set)
|
// must-match. It is reaper-only and has no default; empty (with WorldsHostPath set)
|
||||||
// is rejected fail-loud by the generator.
|
// is rejected fail-loud by the generator.
|
||||||
ArchiveLocalPath string
|
ArchiveLocalPath string
|
||||||
|
// RegistryStorage, UploadsStorage and BackupStorage are the capacities the
|
||||||
|
// registry, uploads and world-archive PVCs request ("20Gi"); empty keeps
|
||||||
|
// 10Gi, 5Gi and 10Gi. A PVC's request can only grow, and only on a class that
|
||||||
|
// allows expansion, so the installer keeps an existing PVC's size. On k3s
|
||||||
|
// local-path the request is a label: the volume is a directory on the node's
|
||||||
|
// disk and nothing stops it outgrowing the number; the registry pruner, the
|
||||||
|
// uploads budget (user_uploads_max_bytes) and the archive cap
|
||||||
|
// (max_local_bytes) are what bound them there.
|
||||||
|
RegistryStorage string
|
||||||
|
UploadsStorage string
|
||||||
|
BackupStorage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate reports a Params the renderer cannot turn into objects.
|
||||||
|
func (p Params) Validate() error {
|
||||||
|
for _, q := range []struct{ name, v string }{
|
||||||
|
{"registry storage", p.RegistryStorage},
|
||||||
|
{"uploads storage", p.UploadsStorage},
|
||||||
|
{"backup storage", p.BackupStorage},
|
||||||
|
} {
|
||||||
|
if q.v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, err := resource.ParseQuantity(q.v)
|
||||||
|
if err != nil || v.Sign() <= 0 {
|
||||||
|
return fmt.Errorf("%s %q is not a positive size such as 20Gi", q.name, q.v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// withDefaults returns a copy of p with zero namespace/registry fields filled.
|
// withDefaults returns a copy of p with zero namespace/registry fields filled.
|
||||||
@@ -198,6 +233,15 @@ func (p Params) withDefaults() Params {
|
|||||||
if p.RegistryImage == "" {
|
if p.RegistryImage == "" {
|
||||||
p.RegistryImage = defaultRegistryImage
|
p.RegistryImage = defaultRegistryImage
|
||||||
}
|
}
|
||||||
|
if p.RegistryStorage == "" {
|
||||||
|
p.RegistryStorage = registryStorageSize
|
||||||
|
}
|
||||||
|
if p.UploadsStorage == "" {
|
||||||
|
p.UploadsStorage = uploadsStorageSize
|
||||||
|
}
|
||||||
|
if p.BackupStorage == "" {
|
||||||
|
p.BackupStorage = backupStorageSize
|
||||||
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1102,7 +1102,7 @@ func registryPVC(p Params) *corev1.PersistentVolumeClaim {
|
|||||||
Spec: corev1.PersistentVolumeClaimSpec{
|
Spec: corev1.PersistentVolumeClaimSpec{
|
||||||
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
||||||
Resources: corev1.VolumeResourceRequirements{
|
Resources: corev1.VolumeResourceRequirements{
|
||||||
Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(registryStorageSize)},
|
Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(p.RegistryStorage)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1123,7 @@ func uploadsPVC(p Params) *corev1.PersistentVolumeClaim {
|
|||||||
Spec: corev1.PersistentVolumeClaimSpec{
|
Spec: corev1.PersistentVolumeClaimSpec{
|
||||||
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
||||||
Resources: corev1.VolumeResourceRequirements{
|
Resources: corev1.VolumeResourceRequirements{
|
||||||
Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(uploadsStorageSize)},
|
Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(p.UploadsStorage)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1147,7 +1147,7 @@ func backupPVC(p Params) *corev1.PersistentVolumeClaim {
|
|||||||
Spec: corev1.PersistentVolumeClaimSpec{
|
Spec: corev1.PersistentVolumeClaimSpec{
|
||||||
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
||||||
Resources: corev1.VolumeResourceRequirements{
|
Resources: corev1.VolumeResourceRequirements{
|
||||||
Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(backupStorageSize)},
|
Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(p.BackupStorage)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1131,3 +1131,37 @@ func TestOperatorMetricsService(t *testing.T) {
|
|||||||
t.Error("Workloads does not render the operator metrics Service")
|
t.Error("Workloads does not render the operator metrics Service")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPVCSizes proves the three claim sizes follow Params and default when unset,
|
||||||
|
// and that Validate refuses a size the API server would reject.
|
||||||
|
func TestPVCSizes(t *testing.T) {
|
||||||
|
sizes := func(p Params) map[string]string {
|
||||||
|
got := map[string]string{}
|
||||||
|
for _, o := range Workloads(p.withDefaults()) {
|
||||||
|
if pvc, ok := o.(*corev1.PersistentVolumeClaim); ok {
|
||||||
|
got[pvc.Name] = pvc.Spec.Resources.Requests.Storage().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return got
|
||||||
|
}
|
||||||
|
p := testParams()
|
||||||
|
p.BackupPVC = "felis-backups"
|
||||||
|
def := sizes(p)
|
||||||
|
if def[registryName] != registryStorageSize || def[uploadsPVCName] != uploadsStorageSize || def["felis-backups"] != backupStorageSize {
|
||||||
|
t.Errorf("default sizes = %v", def)
|
||||||
|
}
|
||||||
|
p.RegistryStorage, p.UploadsStorage, p.BackupStorage = "40Gi", "8Gi", "100Gi"
|
||||||
|
if got := sizes(p); got[registryName] != "40Gi" || got[uploadsPVCName] != "8Gi" || got["felis-backups"] != "100Gi" {
|
||||||
|
t.Errorf("configured sizes = %v", got)
|
||||||
|
}
|
||||||
|
if err := p.Validate(); err != nil {
|
||||||
|
t.Errorf("Validate(%+v) = %v", p, err)
|
||||||
|
}
|
||||||
|
for _, bad := range []string{"lots", "0", "-1Gi"} {
|
||||||
|
q := testParams()
|
||||||
|
q.UploadsStorage = bad
|
||||||
|
if err := q.Validate(); err == nil {
|
||||||
|
t.Errorf("Validate accepted uploads storage %q", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user