Files
Felis/internal/apis/felis/v1alpha1/crd_test.go
T

106 lines
3.9 KiB
Go

package v1alpha1_test
import (
"encoding/json"
"os"
"testing"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
"k8s.io/kube-openapi/pkg/validation/validate"
"sigs.k8s.io/yaml"
)
// crdSchema loads the openAPIV3Schema the cluster enforces, from the same YAML
// `felis bootstrap-assets crd` applies. The x-kubernetes-validations (CEL) rules
// ride along as extensions and are checked live with a server-side dry run.
func crdSchema(t *testing.T) *spec.Schema {
t.Helper()
raw, err := os.ReadFile("../../../../deploy/crd/felis.lolicon.best_minecraftservers.yaml")
if err != nil {
t.Fatal(err)
}
var crd struct {
Spec struct {
Versions []struct {
Schema struct {
OpenAPIV3Schema json.RawMessage `json:"openAPIV3Schema"`
} `json:"schema"`
} `json:"versions"`
} `json:"spec"`
}
if err := yaml.Unmarshal(raw, &crd); err != nil {
t.Fatal(err)
}
if len(crd.Spec.Versions) != 1 {
t.Fatalf("versions = %d, want the single v1alpha1", len(crd.Spec.Versions))
}
var s spec.Schema
if err := json.Unmarshal(crd.Spec.Versions[0].Schema.OpenAPIV3Schema, &s); err != nil {
t.Fatal(err)
}
return &s
}
// server is a minimal valid MinecraftServer with one extra spec section.
func server(section string, body map[string]any) map[string]any {
sp := map[string]any{"image": "paper", "subdomain": "survival"}
if section != "" {
sp[section] = body
}
return map[string]any{
"apiVersion": "felis.lolicon.best/v1alpha1",
"kind": "MinecraftServer",
"metadata": map[string]any{"name": "survival"},
"spec": sp,
}
}
func TestCRDBoundsNumericFields(t *testing.T) {
schema := crdSchema(t)
cases := []struct {
name string
section string
body map[string]any
ok bool
}{
{"defaults", "", nil, true},
{"rcon port 0 means default", "rcon", map[string]any{"port": 0}, true},
{"rcon port 25575", "rcon", map[string]any{"port": 25575}, true},
{"rcon port negative", "rcon", map[string]any{"port": -1}, false},
{"rcon port past 65535", "rcon", map[string]any{"port": 70000}, false},
{"grace period 3600", "lifecycle", map[string]any{"terminationGracePeriodSeconds": 3600}, true},
{"grace period negative", "lifecycle", map[string]any{"terminationGracePeriodSeconds": -1}, false},
{"grace period past an hour", "lifecycle", map[string]any{"terminationGracePeriodSeconds": 3601}, false},
{"startup timeout a day", "startup", map[string]any{"timeoutSeconds": 86400}, true},
{"startup timeout negative", "startup", map[string]any{"timeoutSeconds": -5}, false},
{"startup timeout past a day", "startup", map[string]any{"timeoutSeconds": 100000}, false},
{"readiness timeout negative", "startup", map[string]any{"readinessTimeoutSeconds": -1}, false},
{"readiness timeout past a day", "startup", map[string]any{"readinessTimeoutSeconds": 86401}, false},
{"health port 8080", "startup", map[string]any{"healthHTTPPort": 8080}, true},
{"health port past 65535", "startup", map[string]any{"healthHTTPPort": 70000}, false},
{"health port negative", "startup", map[string]any{"healthHTTPPort": -1}, false},
{"idle a week", "idle", map[string]any{"emptySecondsBeforeStop": 604800}, true},
{"idle negative", "idle", map[string]any{"emptySecondsBeforeStop": -1}, false},
{"idle past a week", "idle", map[string]any{"emptySecondsBeforeStop": 604801}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Round-trip through JSON so numbers arrive as the float64 the
// apiserver's decoder hands the validator.
raw, _ := json.Marshal(server(tc.section, tc.body))
var obj any
if err := json.Unmarshal(raw, &obj); err != nil {
t.Fatal(err)
}
err := validate.AgainstSchema(schema, obj, strfmt.Default)
if tc.ok && err != nil {
t.Fatalf("rejected a valid spec: %v", err)
}
if !tc.ok && err == nil {
t.Fatal("accepted an out-of-range value")
}
})
}
}