The platform package that places servers across nodes and wires the operator, build, restore, and reaper subsystems, plus cmd/felis, the single binary that runs them.
193 lines
7.0 KiB
Go
193 lines
7.0 KiB
Go
package platform
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
|
"felis.lolicon.best/internal/operator"
|
|
networkingv1 "k8s.io/api/networking/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
func npByName(t *testing.T, nps []*networkingv1.NetworkPolicy, name string) *networkingv1.NetworkPolicy {
|
|
t.Helper()
|
|
for _, np := range nps {
|
|
if np.Name == name {
|
|
return np
|
|
}
|
|
}
|
|
t.Fatalf("network policy %q not found", name)
|
|
return nil
|
|
}
|
|
|
|
// TestServerSelector_MatchesOperatorLabels proves the fence selects exactly the
|
|
// pods the operator labels. If the operator ever renamed its label values the
|
|
// policy would silently stop protecting the pods — this couples the two.
|
|
func TestServerSelector_MatchesOperatorLabels(t *testing.T) {
|
|
sel := serverPodSelector()
|
|
want := map[string]string{
|
|
v1alpha1.LabelManagedBy: operator.ManagedByValue,
|
|
v1alpha1.LabelComponent: operator.ComponentValue,
|
|
}
|
|
if len(sel.MatchLabels) != len(want) {
|
|
t.Fatalf("selector has %d labels, want %d", len(sel.MatchLabels), len(want))
|
|
}
|
|
for k, v := range want {
|
|
if sel.MatchLabels[k] != v {
|
|
t.Errorf("selector[%q] = %q, want %q", k, sel.MatchLabels[k], v)
|
|
}
|
|
}
|
|
// Guard against the values being empty (a typo making the selector match-all-ish).
|
|
if operator.ManagedByValue == "" || operator.ComponentValue == "" {
|
|
t.Fatal("operator label values must be non-empty")
|
|
}
|
|
}
|
|
|
|
// TestDefaultDeny enforces the baseline: select all pods, declare Ingress policy,
|
|
// admit nothing.
|
|
func TestDefaultDeny(t *testing.T) {
|
|
nps := MinecraftNetworkPolicies(testParams())
|
|
dd := npByName(t, nps, "felis-default-deny-ingress")
|
|
|
|
if len(dd.Spec.PodSelector.MatchLabels) != 0 || len(dd.Spec.PodSelector.MatchExpressions) != 0 {
|
|
t.Error("default-deny must select ALL pods (empty podSelector)")
|
|
}
|
|
if !hasPolicyType(dd, networkingv1.PolicyTypeIngress) {
|
|
t.Error("default-deny must declare the Ingress policy type")
|
|
}
|
|
if len(dd.Spec.Ingress) != 0 {
|
|
t.Error("default-deny must have NO ingress rules (deny all)")
|
|
}
|
|
}
|
|
|
|
// TestAllowRcon enforces the AND-semantics control-plane peer on port 25575.
|
|
func TestAllowRcon(t *testing.T) {
|
|
nps := MinecraftNetworkPolicies(testParams())
|
|
rcon := npByName(t, nps, "felis-allow-rcon-from-control-plane")
|
|
|
|
// Selects server pods, not all pods.
|
|
if rcon.Spec.PodSelector.MatchLabels[v1alpha1.LabelComponent] != operator.ComponentValue {
|
|
t.Error("rcon policy must select server pods")
|
|
}
|
|
if len(rcon.Spec.Ingress) != 1 {
|
|
t.Fatalf("rcon policy must have exactly 1 ingress rule, got %d", len(rcon.Spec.Ingress))
|
|
}
|
|
rule := rcon.Spec.Ingress[0]
|
|
|
|
// Exactly one peer, combining BOTH selectors (intersection = AND), never two
|
|
// peers (which would be a union/OR — far wider).
|
|
if len(rule.From) != 1 {
|
|
t.Fatalf("rcon peer count = %d, want 1 (AND-combined); 2 would be OR semantics", len(rule.From))
|
|
}
|
|
peer := rule.From[0]
|
|
if peer.NamespaceSelector == nil || peer.PodSelector == nil {
|
|
t.Fatal("rcon peer must set BOTH namespaceSelector AND podSelector")
|
|
}
|
|
if got := peer.NamespaceSelector.MatchLabels["kubernetes.io/metadata.name"]; got != "felis" {
|
|
t.Errorf("rcon namespaceSelector = %q, want control ns felis", got)
|
|
}
|
|
if peer.PodSelector.MatchLabels[LabelPartOf] != controlPlanePartOf {
|
|
t.Error("rcon podSelector must require part-of=felis-control-plane")
|
|
}
|
|
|
|
// Component must be In {api, operator} — and NOT include reaper.
|
|
var compReq *metav1.LabelSelectorRequirement
|
|
for i := range peer.PodSelector.MatchExpressions {
|
|
if peer.PodSelector.MatchExpressions[i].Key == LabelComponent {
|
|
compReq = &peer.PodSelector.MatchExpressions[i]
|
|
}
|
|
}
|
|
if compReq == nil {
|
|
t.Fatal("rcon podSelector must constrain the component label")
|
|
}
|
|
if compReq.Operator != metav1.LabelSelectorOpIn {
|
|
t.Errorf("component requirement operator = %q, want In", compReq.Operator)
|
|
}
|
|
if !contains(compReq.Values, ComponentAPI) || !contains(compReq.Values, ComponentOperator) {
|
|
t.Errorf("component values = %v, want both api and operator", compReq.Values)
|
|
}
|
|
if contains(compReq.Values, ComponentReaper) {
|
|
t.Error("reaper must NOT be allowed to reach RCON")
|
|
}
|
|
|
|
// Port = the operator's default RCON port (single source of truth, so the
|
|
// prober can reach a default-port server through the fence). Pinned to the
|
|
// concrete 25575 too, guarding an accidental change to the operator default.
|
|
if operator.DefaultRconPort != 25575 {
|
|
t.Errorf("operator.DefaultRconPort = %d, want 25575 (conventional RCON port)", operator.DefaultRconPort)
|
|
}
|
|
assertSinglePort(t, rule.Ports, int(operator.DefaultRconPort))
|
|
}
|
|
|
|
// TestAllowGame_WithCIDRs renders the ipBlock allow path on port 25565.
|
|
func TestAllowGame_WithCIDRs(t *testing.T) {
|
|
p := testParams()
|
|
p.VelocityCIDRs = []string{"10.0.0.5/32", "10.0.0.6/32"}
|
|
game := npByName(t, MinecraftNetworkPolicies(p), "felis-allow-game-from-velocity")
|
|
|
|
if len(game.Spec.Ingress) != 1 {
|
|
t.Fatalf("game policy must have 1 ingress rule, got %d", len(game.Spec.Ingress))
|
|
}
|
|
rule := game.Spec.Ingress[0]
|
|
if len(rule.From) != 2 {
|
|
t.Fatalf("game peers = %d, want 2 ipBlocks", len(rule.From))
|
|
}
|
|
for i, peer := range rule.From {
|
|
if peer.IPBlock == nil {
|
|
t.Errorf("game peer %d must be an ipBlock (Velocity is off-cluster, not a pod)", i)
|
|
}
|
|
if peer.PodSelector != nil || peer.NamespaceSelector != nil {
|
|
t.Errorf("game peer %d must NOT use pod/namespace selectors", i)
|
|
}
|
|
}
|
|
if rule.From[0].IPBlock.CIDR != "10.0.0.5/32" {
|
|
t.Errorf("game cidr[0] = %q", rule.From[0].IPBlock.CIDR)
|
|
}
|
|
if game.Spec.PodSelector.MatchLabels[v1alpha1.LabelComponent] != operator.ComponentValue {
|
|
t.Error("game policy must select server pods")
|
|
}
|
|
assertSinglePort(t, rule.Ports, int(operator.GamePort))
|
|
}
|
|
|
|
// TestAllowGame_FailsClosed is the footgun guard: no VelocityCIDRs ⇒ a policy that
|
|
// admits NOBODY (no ingress rule), never an empty-From rule (which K8s reads as
|
|
// allow-all).
|
|
func TestAllowGame_FailsClosed(t *testing.T) {
|
|
p := testParams()
|
|
p.VelocityCIDRs = nil
|
|
game := npByName(t, MinecraftNetworkPolicies(p), "felis-allow-game-from-velocity")
|
|
|
|
if len(game.Spec.Ingress) != 0 {
|
|
t.Fatalf("game policy with no CIDRs must have ZERO ingress rules (fail-closed), got %d", len(game.Spec.Ingress))
|
|
}
|
|
// Still a valid, selecting policy (so it actively denies, paired with default-deny).
|
|
if game.Spec.PodSelector.MatchLabels[v1alpha1.LabelComponent] != operator.ComponentValue {
|
|
t.Error("game policy must still select server pods even when fail-closed")
|
|
}
|
|
if !hasPolicyType(game, networkingv1.PolicyTypeIngress) {
|
|
t.Error("game policy must declare Ingress policy type")
|
|
}
|
|
}
|
|
|
|
func hasPolicyType(np *networkingv1.NetworkPolicy, pt networkingv1.PolicyType) bool {
|
|
for _, t := range np.Spec.PolicyTypes {
|
|
if t == pt {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func assertSinglePort(t *testing.T, ports []networkingv1.NetworkPolicyPort, want int) {
|
|
t.Helper()
|
|
if len(ports) != 1 {
|
|
t.Fatalf("expected exactly 1 port, got %d", len(ports))
|
|
}
|
|
if ports[0].Port == nil || ports[0].Port.IntValue() != want {
|
|
t.Errorf("port = %v, want %d", ports[0].Port, want)
|
|
}
|
|
if ports[0].Protocol == nil || *ports[0].Protocol != "TCP" {
|
|
t.Errorf("protocol = %v, want TCP", ports[0].Protocol)
|
|
}
|
|
}
|