refactor(deploy)!: TUI
This commit is contained in:
37 files changed
+3622
-1303
No files matched your search
@@ -35,11 +35,9 @@ type Object interface {
|
||||
// make the SAs and NetworkPolicy peers refer to something real (see workloads.go).
|
||||
// The reaper CronJob is also part of Workloads, rendered only when the retention
|
||||
// storage topology is supplied (WorldsHostPath + BackupPVC + ArchiveLocalPath —
|
||||
// workloads.go documents the gate and the shape-asserted hostPath caveat). Still
|
||||
// deliberately NOT rendered: a felis-api Service (its exposure is an out-of-band
|
||||
// deployment choice and nothing in-tree dials it). The per-server StatefulSet is
|
||||
// never a static manifest — the operator renders it at reconcile time
|
||||
// (internal/operator).
|
||||
// workloads.go documents the gate and the shape-asserted hostPath caveat). The
|
||||
// per-server StatefulSet is never a static manifest — the operator renders it at
|
||||
// reconcile time (internal/operator).
|
||||
func Objects(p Params) []Object {
|
||||
p = p.withDefaults()
|
||||
var objs []Object
|
||||
|
||||
@@ -53,6 +53,7 @@ const (
|
||||
DefaultControlNamespace = "felis"
|
||||
DefaultMinecraftNamespace = "minecraft"
|
||||
DefaultBuildNamespace = "felis-build"
|
||||
DefaultPanelNodePort = int32(30443)
|
||||
|
||||
defaultRegistryPort int32 = 5000
|
||||
|
||||
@@ -88,6 +89,9 @@ type Params struct {
|
||||
// control namespace (registry co-located with the control plane).
|
||||
RegistryNamespace string
|
||||
RegistryPort int32
|
||||
// PanelNodePort exposes the built-in HTTPS panel/API origin from the node.
|
||||
// It defaults to 30443 so a fresh setup can finish with a concrete browser URL.
|
||||
PanelNodePort int32
|
||||
// PackageSourceCIDRs is the explicit package-mirror egress allowlist for build
|
||||
// Pods (spec §16). Empty means no internet egress at all — the locked-down
|
||||
// default the build subsystem already enforces.
|
||||
@@ -159,6 +163,9 @@ func (p Params) withDefaults() Params {
|
||||
if p.RegistryPort == 0 {
|
||||
p.RegistryPort = defaultRegistryPort
|
||||
}
|
||||
if p.PanelNodePort == 0 {
|
||||
p.PanelNodePort = DefaultPanelNodePort
|
||||
}
|
||||
if p.RegistryImage == "" {
|
||||
p.RegistryImage = defaultRegistryImage
|
||||
}
|
||||
|
||||
@@ -40,15 +40,6 @@ import (
|
||||
// the worlds to one node implicitly; a multi-node deployment MUST add one (or the
|
||||
// CronJob could schedule on a node where the hostPath is empty) — a hazard left on
|
||||
// record here until multi-node retention is built.
|
||||
//
|
||||
// Deliberately NOT rendered:
|
||||
// - A Service for felis-api. Its external face (8080) is exposed out-of-band
|
||||
// (Ingress/LoadBalancer is a deployment choice) and its internal face's only
|
||||
// consumer is the Velocity plugin; nothing in-tree dials a felis-api Service
|
||||
// name, so rendering one would be a speculative selector. The registry Service
|
||||
// IS rendered because registry.<ns>.svc:5000 is a pinned consumer hardcoded
|
||||
// across the build subsystem and config.
|
||||
|
||||
const (
|
||||
// configSecretName / serviceTokenSecretName are referenced BY NAME and NEVER
|
||||
// rendered into the bundle: felis.toml carries the database URL (a credential)
|
||||
@@ -59,6 +50,7 @@ const (
|
||||
configSecretKey = "felis.toml"
|
||||
configMountPath = "/etc/felis"
|
||||
configFilePath = "/etc/felis/felis.toml"
|
||||
felisBinaryPath = "/usr/local/bin/felis"
|
||||
serviceTokenSecretName = "felis-service-token"
|
||||
serviceTokenSecretKey = "token"
|
||||
|
||||
@@ -67,7 +59,10 @@ const (
|
||||
// agreement lives in the out-of-band config Secret and cannot be enforced here.
|
||||
apiExternalPort int32 = 8080
|
||||
apiInternalPort int32 = 8081
|
||||
apiHTTPSPort int32 = 8443
|
||||
operatorMetricsPort int32 = 8080
|
||||
apiTLSSecretName = "felis-api-tls"
|
||||
apiTLSMountPath = "/etc/felis/tls"
|
||||
|
||||
registryName = "registry"
|
||||
registryDataPath = "/var/lib/registry"
|
||||
@@ -113,6 +108,7 @@ func Workloads(p Params) []Object {
|
||||
p = p.withDefaults()
|
||||
objs := []Object{
|
||||
APIDeployment(p),
|
||||
apiService(p),
|
||||
OperatorDeployment(p),
|
||||
registryDeployment(p),
|
||||
registryService(p),
|
||||
@@ -169,18 +165,23 @@ func APIDeployment(p Params) *appsv1.Deployment {
|
||||
container := corev1.Container{
|
||||
Name: ComponentAPI,
|
||||
Image: p.FelisImage,
|
||||
Command: []string{"felis", "api"},
|
||||
Command: []string{felisBinaryPath, "api"},
|
||||
Args: []string{
|
||||
"--config", configFilePath,
|
||||
"--internal-addr", fmt.Sprintf(":%d", apiInternalPort),
|
||||
"--https-addr", fmt.Sprintf(":%d", apiHTTPSPort),
|
||||
"--tls-cert", apiTLSMountPath + "/tls.crt",
|
||||
"--tls-key", apiTLSMountPath + "/tls.key",
|
||||
},
|
||||
Env: env,
|
||||
Ports: []corev1.ContainerPort{
|
||||
{Name: "external", ContainerPort: apiExternalPort, Protocol: corev1.ProtocolTCP},
|
||||
{Name: "https", ContainerPort: apiHTTPSPort, Protocol: corev1.ProtocolTCP},
|
||||
{Name: "internal", ContainerPort: apiInternalPort, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{Name: configVolume, MountPath: configMountPath, ReadOnly: true},
|
||||
{Name: "tls", MountPath: apiTLSMountPath, ReadOnly: true},
|
||||
{Name: tmpVolume, MountPath: "/tmp"},
|
||||
},
|
||||
Resources: controlPlaneResources(),
|
||||
@@ -194,12 +195,39 @@ func APIDeployment(p Params) *appsv1.Deployment {
|
||||
Secret: &corev1.SecretVolumeSource{SecretName: configSecretName},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tls",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{SecretName: apiTLSSecretName},
|
||||
},
|
||||
},
|
||||
{Name: tmpVolume, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
|
||||
}
|
||||
|
||||
return controlPlaneDeployment(p, SAAPI, container, volumes)
|
||||
}
|
||||
|
||||
// apiService exposes the built-in HTTPS panel/API origin as a stable NodePort.
|
||||
func apiService(p Params) *corev1.Service {
|
||||
p = p.withDefaults()
|
||||
labels := controlPlanePodLabels(ComponentAPI)
|
||||
return &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: SAAPI, Namespace: p.ControlNamespace, Labels: labels},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Type: corev1.ServiceTypeNodePort,
|
||||
Selector: labels,
|
||||
Ports: []corev1.ServicePort{{
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
TargetPort: intstr.FromString("https"),
|
||||
NodePort: p.PanelNodePort,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OperatorDeployment renders the felis-operator Deployment (spec §5). It runs as
|
||||
// the felis-operator SA and carries controlPlanePodLabels(operator), the second
|
||||
// pod the allow-rcon peer admits (the readiness prober dials RCON). It takes NO
|
||||
@@ -213,7 +241,7 @@ func OperatorDeployment(p Params) *appsv1.Deployment {
|
||||
container := corev1.Container{
|
||||
Name: ComponentOperator,
|
||||
Image: p.FelisImage,
|
||||
Command: []string{"felis", "operator"},
|
||||
Command: []string{felisBinaryPath, "operator"},
|
||||
Args: []string{
|
||||
"--namespace", p.MinecraftNamespace,
|
||||
"--metrics-bind-address", fmt.Sprintf(":%d", operatorMetricsPort),
|
||||
@@ -271,7 +299,7 @@ func reaperCronJob(p Params) *batchv1.CronJob {
|
||||
container := corev1.Container{
|
||||
Name: ComponentReaper,
|
||||
Image: p.FelisImage,
|
||||
Command: []string{"felis", "reaper"},
|
||||
Command: []string{felisBinaryPath, "reaper"},
|
||||
Args: []string{
|
||||
"--config", configFilePath,
|
||||
"--worlds-root", worldsMountPath,
|
||||
|
||||
@@ -153,8 +153,8 @@ func TestAPIDeployment_Wiring(t *testing.T) {
|
||||
d := APIDeployment(p)
|
||||
ps, c := podSpec(t, d)
|
||||
|
||||
if got := append(append([]string{}, c.Command...), c.Args...); !containsSeq(got, []string{"felis", "api"}) {
|
||||
t.Errorf("api command/args = %v, want it to start `felis api`", got)
|
||||
if got := append(append([]string{}, c.Command...), c.Args...); !containsSeq(got, []string{felisBinaryPath, "api"}) {
|
||||
t.Errorf("api command/args = %v, want it to start `%s api`", got, felisBinaryPath)
|
||||
}
|
||||
if !contains(c.Args, "--config") || !contains(c.Args, configFilePath) {
|
||||
t.Errorf("api args must mount config at %s, got %v", configFilePath, c.Args)
|
||||
@@ -162,6 +162,13 @@ func TestAPIDeployment_Wiring(t *testing.T) {
|
||||
if !contains(c.Args, "--internal-addr") {
|
||||
t.Errorf("api args must set --internal-addr, got %v", c.Args)
|
||||
}
|
||||
if !contains(c.Args, "--https-addr") || !contains(c.Args, ":8443") {
|
||||
t.Errorf("api args must set HTTPS listener, got %v", c.Args)
|
||||
}
|
||||
if !contains(c.Args, "--tls-cert") || !contains(c.Args, apiTLSMountPath+"/tls.crt") ||
|
||||
!contains(c.Args, "--tls-key") || !contains(c.Args, apiTLSMountPath+"/tls.key") {
|
||||
t.Errorf("api args must point at mounted TLS secret, got %v", c.Args)
|
||||
}
|
||||
if c.Image != p.FelisImage {
|
||||
t.Errorf("api image = %q, want FelisImage %q", c.Image, p.FelisImage)
|
||||
}
|
||||
@@ -194,6 +201,13 @@ func TestAPIDeployment_Wiring(t *testing.T) {
|
||||
if m := mountByName(c.VolumeMounts, configVolume); m == nil || !m.ReadOnly {
|
||||
t.Error("config volume must be mounted read-only")
|
||||
}
|
||||
tlsVol := volumeByName(ps.Volumes, "tls")
|
||||
if tlsVol == nil || tlsVol.Secret == nil || tlsVol.Secret.SecretName != apiTLSSecretName {
|
||||
t.Fatalf("tls volume must mount Secret %q, got %#v", apiTLSSecretName, tlsVol)
|
||||
}
|
||||
if m := mountByName(c.VolumeMounts, "tls"); m == nil || !m.ReadOnly || m.MountPath != apiTLSMountPath {
|
||||
t.Errorf("tls volume mount = %#v, want read-only at %s", m, apiTLSMountPath)
|
||||
}
|
||||
|
||||
// No backup PVC in testParams ⇒ no FELIS_BACKUP_PVC env (restore degrades to 503).
|
||||
if envVar(c.Env, "FELIS_BACKUP_PVC") != nil {
|
||||
@@ -201,6 +215,30 @@ func TestAPIDeployment_Wiring(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIService_NodePort(t *testing.T) {
|
||||
p := testParams()
|
||||
p.PanelNodePort = 30445
|
||||
svc := apiService(p)
|
||||
dep := APIDeployment(p)
|
||||
|
||||
if svc.Name != SAAPI || svc.Namespace != p.ControlNamespace {
|
||||
t.Errorf("api Service = %s/%s, want %s/%s", svc.Namespace, svc.Name, p.ControlNamespace, SAAPI)
|
||||
}
|
||||
if svc.Spec.Type != corev1.ServiceTypeNodePort {
|
||||
t.Errorf("api Service type = %s, want NodePort", svc.Spec.Type)
|
||||
}
|
||||
if !mapSelectorMatches(svc.Spec.Selector, dep.Spec.Template.Labels) {
|
||||
t.Errorf("api Service selector %v does not select api pod labels %v", svc.Spec.Selector, dep.Spec.Template.Labels)
|
||||
}
|
||||
if len(svc.Spec.Ports) != 1 {
|
||||
t.Fatalf("api Service ports = %v, want one", svc.Spec.Ports)
|
||||
}
|
||||
port := svc.Spec.Ports[0]
|
||||
if port.Port != 443 || port.TargetPort.StrVal != "https" || port.NodePort != p.PanelNodePort {
|
||||
t.Errorf("api Service port = %#v, want 443 -> https NodePort %d", port, p.PanelNodePort)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIDeployment_BackupPVC proves the FELIS_BACKUP_PVC env appears only when a
|
||||
// backup PVC is named.
|
||||
func TestAPIDeployment_BackupPVC(t *testing.T) {
|
||||
@@ -219,8 +257,8 @@ func TestOperatorDeployment_Wiring(t *testing.T) {
|
||||
d := OperatorDeployment(p)
|
||||
ps, c := podSpec(t, d)
|
||||
|
||||
if got := append(append([]string{}, c.Command...), c.Args...); !containsSeq(got, []string{"felis", "operator"}) {
|
||||
t.Errorf("operator command/args = %v, want it to start `felis operator`", got)
|
||||
if got := append(append([]string{}, c.Command...), c.Args...); !containsSeq(got, []string{felisBinaryPath, "operator"}) {
|
||||
t.Errorf("operator command/args = %v, want it to start `%s operator`", got, felisBinaryPath)
|
||||
}
|
||||
if !contains(c.Args, "--namespace") || !contains(c.Args, p.MinecraftNamespace) {
|
||||
t.Errorf("operator must watch --namespace %s, got %v", p.MinecraftNamespace, c.Args)
|
||||
@@ -299,12 +337,12 @@ func TestRegistry_DeploymentServicePVC(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestWorkloads_BundleContents sanity-checks the slice Workloads returns: the two
|
||||
// control-plane Deployments + the registry Deployment/Service/PVC, every one with
|
||||
// TypeMeta (so its YAML header renders).
|
||||
// control-plane Deployments, the api Service, and the registry Deployment/Service/PVC,
|
||||
// every one with TypeMeta (so its YAML header renders).
|
||||
func TestWorkloads_BundleContents(t *testing.T) {
|
||||
objs := Workloads(testParams())
|
||||
if len(objs) != 5 {
|
||||
t.Fatalf("Workloads returned %d objects, want 5", len(objs))
|
||||
if len(objs) != 6 {
|
||||
t.Fatalf("Workloads returned %d objects, want 6", len(objs))
|
||||
}
|
||||
for _, o := range objs {
|
||||
gvk := o.GetObjectKind().GroupVersionKind()
|
||||
@@ -444,9 +482,9 @@ func TestReaperCronJob_Shape(t *testing.T) {
|
||||
t.Error("reaper container must drop ALL capabilities")
|
||||
}
|
||||
|
||||
// Entrypoint: `felis reaper --config <cfg> --worlds-root /worlds`.
|
||||
if got := append(append([]string{}, c.Command...), c.Args...); !containsSeq(got, []string{"felis", "reaper"}) {
|
||||
t.Errorf("reaper command/args = %v, want it to start `felis reaper`", got)
|
||||
// Entrypoint: `/usr/local/bin/felis reaper --config <cfg> --worlds-root /worlds`.
|
||||
if got := append(append([]string{}, c.Command...), c.Args...); !containsSeq(got, []string{felisBinaryPath, "reaper"}) {
|
||||
t.Errorf("reaper command/args = %v, want it to start `%s reaper`", got, felisBinaryPath)
|
||||
}
|
||||
if !contains(c.Args, "--config") || !contains(c.Args, configFilePath) {
|
||||
t.Errorf("reaper must read config at %s, got %v", configFilePath, c.Args)
|
||||
|
||||
Reference in new issue
Block a user