fix(operator): 服务器 pod 先等出站围栏生效再启动镜像,新 pod 第一个请求不再能连到内网
This commit is contained in:
5 files changed
+160
-20
No files matched your search
@@ -270,13 +270,15 @@ func buildStatefulSet(server *v1alpha1.MinecraftServer, replicas int32, felisIma
|
||||
// otherwise be read-only to it. An arbitrary user Paper image then gets the forwarding
|
||||
// config written for it (it does not consume FELIS_FORWARDING_SECRET itself); system
|
||||
// servers (login/lobby) are Felis-built and handle forwarding in their own
|
||||
// entrypoints. Without a felis image name there is nothing to run either step with.
|
||||
// entrypoints. Last, every server waits for its egress fence (egressGateInitContainer).
|
||||
// Without a felis image name there is nothing to run any step with.
|
||||
var initContainers []corev1.Container
|
||||
if felisImage != "" {
|
||||
initContainers = append(initContainers, prepareDataInitContainer(felisImage))
|
||||
if server.Labels[v1alpha1.LabelSystemRole] == "" {
|
||||
initContainers = append(initContainers, forwardingInitContainer(felisImage))
|
||||
}
|
||||
initContainers = append(initContainers, egressGateInitContainer(felisImage))
|
||||
}
|
||||
|
||||
grace := graceSeconds(server)
|
||||
@@ -446,6 +448,35 @@ func forwardingInitContainer(felisImage string) corev1.Container {
|
||||
}
|
||||
}
|
||||
|
||||
// egressGateWait bounds how long a server's start waits for its egress fence.
|
||||
// kube-router programs a new pod's policy within a second; the rest is margin for
|
||||
// a loaded node.
|
||||
const egressGateWait = 30 * time.Second
|
||||
|
||||
// egressGateInitContainer holds the server image back until the pod's egress fence
|
||||
// (platform.ServerEgressPolicies) is in effect. The policy is programmed after the
|
||||
// pod starts, and live on k3s a new server-labelled pod reached felis-api's
|
||||
// internal face on its first request; the server image and the plugins it loads
|
||||
// are the owner's code and must never run inside that window. The gate is `felis
|
||||
// egress-gate`, which the build pod runs first for the same reason.
|
||||
//
|
||||
// It fails open where the build's gate fails closed. An operator's
|
||||
// --server-egress-allow-cidr can cover the node the Kubernetes API Service leads
|
||||
// to, and then the probe answers forever while the fence stands; refusing would
|
||||
// take every server down on a legitimate install. After egressGateWait the policy
|
||||
// has landed if it ever will, so letting the server on costs nothing the fence
|
||||
// would have given, and the gate's log says why the start was slow.
|
||||
func egressGateInitContainer(felisImage string) corev1.Container {
|
||||
return corev1.Container{
|
||||
Name: "egress-gate",
|
||||
Image: felisImage,
|
||||
Command: []string{felisBinaryPath, "egress-gate",
|
||||
"--wait", egressGateWait.String(), "--fail-open"},
|
||||
Resources: initContainerResources(),
|
||||
SecurityContext: hardenedContainerSecurityContext(true),
|
||||
}
|
||||
}
|
||||
|
||||
// prepareDataInitContainer runs `felis init-volume`, which chowns every world-volume
|
||||
// entry not already owned by naming.GameUID:GameGID. It is the one container in the
|
||||
// pod that runs as root, and it holds only what a chown walk needs: CHOWN to change
|
||||
@@ -512,8 +543,8 @@ func hardenedContainerSecurityContext(readOnlyRoot bool) *corev1.SecurityContext
|
||||
return sc
|
||||
}
|
||||
|
||||
// initContainerResources bounds the two felis-image initContainers. Both are short
|
||||
// file walks; the memory ceiling stops a pathological volume from taking the node's
|
||||
// initContainerResources bounds the felis-image initContainers. Two are short file
|
||||
// walks and the third a dial loop; the memory ceiling stops a pathological volume from taking the node's
|
||||
// memory with it, and no CPU limit keeps a large world's chown from being throttled
|
||||
// into the pod's start-up time.
|
||||
func initContainerResources() corev1.ResourceRequirements {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
||||
@@ -58,7 +59,7 @@ func TestReadinessProbeHTTPCustomPath(t *testing.T) {
|
||||
|
||||
// A user server (no system-role label) gets the forwarding-config initContainer after
|
||||
// prepare-data, running the felis image and mounting the world volume. A system
|
||||
// server gets only prepare-data, and a build with no felis image name gets neither.
|
||||
// server gets no forwarding step, and a build with no felis image name gets no step.
|
||||
func TestBuildStatefulSetForwardingInitContainer(t *testing.T) {
|
||||
user := &v1alpha1.MinecraftServer{}
|
||||
user.Spec.Storage.Size = "1Gi"
|
||||
@@ -68,8 +69,8 @@ func TestBuildStatefulSetForwardingInitContainer(t *testing.T) {
|
||||
t.Fatalf("buildStatefulSet: %v", err)
|
||||
}
|
||||
inits := sts.Spec.Template.Spec.InitContainers
|
||||
if len(inits) != 2 || inits[0].Name != "prepare-data" || inits[1].Name != "init-forwarding" {
|
||||
t.Fatalf("want [prepare-data init-forwarding], got %+v", inits)
|
||||
if len(inits) != 3 || inits[0].Name != "prepare-data" || inits[1].Name != "init-forwarding" || inits[2].Name != "egress-gate" {
|
||||
t.Fatalf("want [prepare-data init-forwarding egress-gate], got %+v", inits)
|
||||
}
|
||||
ic := inits[1]
|
||||
if ic.Image != "felis:demo" {
|
||||
@@ -113,13 +114,51 @@ func TestBuildStatefulSetForwardingInitContainer(t *testing.T) {
|
||||
}
|
||||
|
||||
// System server handles forwarding in its own entrypoint, but its world still
|
||||
// needs handing to the game uid.
|
||||
// needs handing to the game uid and its image waits for the fence all the same.
|
||||
sys := &v1alpha1.MinecraftServer{}
|
||||
sys.Spec.Storage.Size = "1Gi"
|
||||
sys.Labels = map[string]string{v1alpha1.LabelSystemRole: "lobby"}
|
||||
sysSts, _ := buildStatefulSet(sys, 1, "felis:demo")
|
||||
if got := sysSts.Spec.Template.Spec.InitContainers; len(got) != 1 || got[0].Name != "prepare-data" {
|
||||
t.Errorf("system server must get only prepare-data, got %+v", got)
|
||||
if got := sysSts.Spec.Template.Spec.InitContainers; len(got) != 2 || got[0].Name != "prepare-data" || got[1].Name != "egress-gate" {
|
||||
t.Errorf("system server must get [prepare-data egress-gate], got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Every server's image waits behind the egress gate, the last step before it: the
|
||||
// pod's NetworkPolicy is programmed after the pod starts. The gate runs the felis
|
||||
// image with nothing but the dial it needs, and it lets the server start after
|
||||
// egressGateWait, where the build's gate refuses.
|
||||
func TestBuildStatefulSetGatesEgress(t *testing.T) {
|
||||
for _, role := range []string{"", naming.SystemLoginServer, "lobby"} {
|
||||
server := &v1alpha1.MinecraftServer{}
|
||||
server.Spec.Storage.Size = "1Gi"
|
||||
if role != "" {
|
||||
server.Labels = map[string]string{v1alpha1.LabelSystemRole: role}
|
||||
}
|
||||
sts, err := buildStatefulSet(server, 1, "felis:demo")
|
||||
if err != nil {
|
||||
t.Fatalf("buildStatefulSet: %v", err)
|
||||
}
|
||||
inits := sts.Spec.Template.Spec.InitContainers
|
||||
gate := inits[len(inits)-1]
|
||||
if gate.Name != "egress-gate" || gate.Image != "felis:demo" {
|
||||
t.Fatalf("role %q: last init container = %s (%s), want egress-gate on the felis image", role, gate.Name, gate.Image)
|
||||
}
|
||||
want := []string{felisBinaryPath, "egress-gate", "--wait", "30s", "--fail-open"}
|
||||
if !slices.Equal(gate.Command, want) || len(gate.Args) != 0 {
|
||||
t.Errorf("role %q: gate runs %v %v, want %v", role, gate.Command, gate.Args, want)
|
||||
}
|
||||
if sc := gate.SecurityContext; sc == nil || sc.RunAsUser != nil || !dropsAll(sc) || len(sc.Capabilities.Add) != 0 ||
|
||||
sc.ReadOnlyRootFilesystem == nil || !*sc.ReadOnlyRootFilesystem ||
|
||||
sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
|
||||
t.Errorf("role %q: the gate must run unprivileged as the pod uid, got %+v", role, sc)
|
||||
}
|
||||
if len(gate.VolumeMounts) != 0 || len(gate.Env) != 0 || len(gate.EnvFrom) != 0 {
|
||||
t.Errorf("role %q: the gate needs no volume and no secret, got mounts %+v env %+v %+v", role, gate.VolumeMounts, gate.Env, gate.EnvFrom)
|
||||
}
|
||||
if gate.Resources.Limits.Memory().IsZero() {
|
||||
t.Errorf("role %q: the gate must carry a memory limit", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user