fix(images): 服务器镜像在创建时固定到仓库 digest,更换镜像需确认备份,安装器重建前先固定旧服并推送不可变版本标签

This commit is contained in:
Lemon-miaow committed 2026-09-24 16:57:38 +08:00
1 parent 857b6da886
commit 215bfd78d7
29 files changed
+1149 -29

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"strings"
"time"
"felis.lolicon.best/internal/apis/felis/v1alpha1"
"felis.lolicon.best/internal/imagepin"
"felis.lolicon.best/internal/platform"
"k8s.io/apimachinery/pkg/api/meta"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// defaultRegistryURL is the [registry] url every install uses; deploy/bootstrap.sh
// spells the same value as REGISTRY_URL.
const defaultRegistryURL = "registry.felis.svc:5000"
// cmdPinImages pins every user server whose spec.image still names a mutable tag
// in the platform registry to the digest that tag names now (internal/imagepin).
// felis-api pins on create, so this covers the servers created before it did.
//
// deploy/bootstrap.sh runs it before it rebuilds the game images and pushes them
// over the same tags: run after the push, it would pin those servers to the new
// build, which is exactly the silent Minecraft upgrade pinning exists to stop.
// It reaches the registry through the node's loopback hostPort, the same way the
// installer pushes.
func cmdPinImages(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("pin-images", flag.ContinueOnError)
fs.SetOutput(stderr)
namespace := fs.String("namespace", platform.DefaultMinecraftNamespace, "namespace the MinecraftServers live in")
registry := fs.String("registry", defaultRegistryURL, "registry host[:port] the image refs spell")
endpoint := fs.String("endpoint", "", "host[:port] to reach the registry at (default: 127.0.0.1 on the registry's port, its hostPort on this node)")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if *endpoint == "" {
*endpoint = loopbackEndpoint(*registry)
}
cl, err := buildSystemServerClient()
if err != nil {
fmt.Fprintf(stderr, "felis pin-images: %v\n", err)
return 1
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
outcomes, err := pinUserServerImages(ctx, cl, *namespace, imagepin.Resolver{Registry: *registry, Endpoint: *endpoint})
if meta.IsNoMatchError(err) {
fmt.Fprintln(stdout, "felis pin-images: no MinecraftServer CRD yet, so no server to pin")
return 0
}
if err != nil {
fmt.Fprintf(stderr, "felis pin-images: %v\n", err)
return 1
}
if len(outcomes) == 0 {
fmt.Fprintln(stdout, "felis pin-images: every user server already runs a pinned image")
return 0
}
fmt.Fprintln(stdout, "felis pin-images: pinning user servers to the build their tag names now:")
exit := 0
for _, o := range outcomes {
if o.err != nil {
fmt.Fprintf(stdout, " - %s: ERROR %v\n", o.name, o.err)
exit = 1
continue
}
fmt.Fprintf(stdout, " - %s: %s\n", o.name, strings.Join(o.changes, ", "))
}
return exit
}
// loopbackEndpoint is the registry's port on 127.0.0.1: the registry Deployment
// binds it as a hostPort, and containerd's mirror and the installer's pushes use
// the same address.
func loopbackEndpoint(registry string) string {
if i := strings.LastIndex(registry, ":"); i >= 0 {
return "127.0.0.1" + registry[i:]
}
return "127.0.0.1"
}
// pinUserServerImages patches spec.image of every user server whose image the
// resolver covers and is not yet pinned. System servers are left on their tags:
// the installer rebuilds and restarts them on purpose (restart_existing_system_servers).
// A server that is already pinned, or runs an image from elsewhere, produces no
// outcome, so a pinned fleet reports nothing. A running server restarts once as
// the operator rolls its StatefulSet onto the pinned ref, which is the build it
// already runs.
func pinUserServerImages(ctx context.Context, cl client.Client, namespace string, r imagepin.Resolver) ([]systemServerOutcome, error) {
var list v1alpha1.MinecraftServerList
if err := cl.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return nil, fmt.Errorf("list servers: %w", err)
}
var out []systemServerOutcome
for i := range list.Items {
ms := &list.Items[i]
if ms.Labels[v1alpha1.LabelSystemRole] != "" || imagepin.Pinned(ms.Spec.Image) || !r.Covers(ms.Spec.Image) {
continue
}
pinned, err := r.Pin(ctx, ms.Spec.Image)
if errors.Is(err, imagepin.ErrNotFound) {
err = fmt.Errorf("%s is not in the registry, so there is no build to pin it to; left unpinned: %w", ms.Spec.Image, err)
}
if err != nil {
out = append(out, systemServerOutcome{name: ms.Name, err: err})
continue
}
patch := client.MergeFrom(ms.DeepCopy())
ms.Spec.Image = pinned
if err := cl.Patch(ctx, ms, patch); err != nil {
out = append(out, systemServerOutcome{name: ms.Name, err: fmt.Errorf("patch %s: %w", ms.Name, err)})
continue
}
out = append(out, systemServerOutcome{name: ms.Name, available: true, updated: true,
changes: []string{"spec.image pinned to " + pinned}})
}
return out, nil
}