diff --git a/internal/apis/felis/v1alpha1/groupversion_info.go b/internal/apis/felis/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..98cc251 --- /dev/null +++ b/internal/apis/felis/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +// Package v1alpha1 contains the felis.lolicon.best/v1alpha1 API group, whose +// MinecraftServer kind is the lifecycle source-of-truth for Felis (spec §1, §4). +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + // GroupName is the hardcoded software-identity API group (spec §2, §4). + GroupName = "felis.lolicon.best" + // Version is the API version served by this package. + Version = "v1alpha1" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + + // SchemeBuilder registers the API types into a runtime.Scheme. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &MinecraftServer{}, + &MinecraftServerList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/internal/apis/felis/v1alpha1/minecraftserver_types.go b/internal/apis/felis/v1alpha1/minecraftserver_types.go new file mode 100644 index 0000000..9b1f99e --- /dev/null +++ b/internal/apis/felis/v1alpha1/minecraftserver_types.go @@ -0,0 +1,257 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Label and annotation keys owned by the operator (spec §4, §5). All keys are +// scoped under the software-identity group so they never collide with the +// deployment domain. +const ( + // LabelServer marks every operator-managed object with its owning + // MinecraftServer name (used as the StatefulSet/Service/PVC selector). + LabelServer = GroupName + "/server" + // LabelManagedBy marks objects reconciled by felis-operator. + LabelManagedBy = GroupName + "/managed-by" + // LabelComponent distinguishes the workload role (server, rcon, ...). + LabelComponent = GroupName + "/component" +) + +// DesiredState is the operator-facing intent toggle (spec §4 spec.desiredState). +type DesiredState string + +const ( + // DesiredRunning asks the operator to bring the server up. + DesiredRunning DesiredState = "Running" + // DesiredStopped asks the operator to scale the server down to zero. + DesiredStopped DesiredState = "Stopped" +) + +// Phase is the observed lifecycle phase (spec §4 status.phase). +type Phase string + +const ( + PhaseUnknown Phase = "Unknown" + PhaseStopped Phase = "Stopped" + PhaseStarting Phase = "Starting" + PhaseRunning Phase = "Running" + PhaseStopping Phase = "Stopping" + PhaseFailed Phase = "Failed" +) + +// AutostartPolicy controls who may wake a stopped server (spec §4, §8). It only +// has meaning when the Velocity proxy runs with online-mode=true. +type AutostartPolicy string + +const ( + // AutostartPublic lets any authenticated player wake the server. + AutostartPublic AutostartPolicy = "public" + // AutostartAllowlist restricts waking to entries in server_allowlist. + AutostartAllowlist AutostartPolicy = "allowlist" + // AutostartOwnerOnly restricts waking to the claimed owner. + AutostartOwnerOnly AutostartPolicy = "ownerOnly" +) + +// EndpointMode describes how the proxy reaches a Running server (spec §4 +// status.endpoint.mode). +type EndpointMode string + +const ( + // EndpointDirect means the proxy dials the Service ClusterIP directly. + EndpointDirect EndpointMode = "direct" + // EndpointFallback means traffic is routed to the configured fallback. + EndpointFallback EndpointMode = "fallback" +) + +// Condition type strings surfaced on status.conditions (spec §4). +const ( + ConditionReady = "Ready" + ConditionRconReached = "RconReached" + ConditionProvisioned = "Provisioned" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// MinecraftServer is the lifecycle source-of-truth for a single managed +// Minecraft server (spec §4). The operator reconciles the StatefulSet, Service +// and PVC from this object; readiness is gated exclusively on an RCON probe. +type MinecraftServer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MinecraftServerSpec `json:"spec,omitempty"` + Status MinecraftServerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// MinecraftServerList is a list of MinecraftServer objects. +type MinecraftServerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MinecraftServer `json:"items"` +} + +// MinecraftServerSpec is the desired state (spec §4 spec.*). +type MinecraftServerSpec struct { + // Subdomain is the per-server label under the deployment zone. It is the + // only routing identity; the operator never hardcodes the parent domain. + Subdomain string `json:"subdomain"` + // DisplayName is the human-facing name shown in the panel and MOTD. + DisplayName string `json:"displayName,omitempty"` + // Description is free-form operator/owner notes. + Description string `json:"description,omitempty"` + // ReaperExempt opts this server out of the world reaper entirely (spec §18). + ReaperExempt bool `json:"reaperExempt,omitempty"` + + // DesiredState toggles the server up or down (default Stopped). + // +kubebuilder:validation:Enum=Running;Stopped + DesiredState DesiredState `json:"desiredState,omitempty"` + // AutostartPolicy controls who may wake the server (spec §8). + // +kubebuilder:validation:Enum=public;allowlist;ownerOnly + AutostartPolicy AutostartPolicy `json:"autostartPolicy,omitempty"` + // FallbackServer is the Velocity server name traffic routes to while this + // server is stopped or starting. + FallbackServer string `json:"fallbackServer,omitempty"` + + // Image is the fully-qualified container image (loader-agnostic). + Image string `json:"image"` + // Jar is the server jar path/name inside the image, if the entrypoint + // needs it explicitly. + Jar string `json:"jar,omitempty"` + // JavaMemory is the heap sizing passed as -Xmx/-Xms (e.g. "4G"). + JavaMemory string `json:"javaMemory,omitempty"` + // JavaFlags are additional JVM flags (e.g. Aikar's flags). + JavaFlags []string `json:"javaFlags,omitempty"` + // Args are extra arguments appended after the jar. + Args []string `json:"args,omitempty"` + // Env are extra environment variables injected into the server container. + Env []EnvVar `json:"env,omitempty"` + + // OnlineMode mirrors server.properties online-mode. Wake/claim semantics + // only hold when the Velocity proxy enforces online-mode=true (spec §8). + OnlineMode bool `json:"onlineMode,omitempty"` + // Motd holds the per-phase MOTD strings surfaced to status pings. + Motd MotdSpec `json:"motd,omitempty"` + + // Rcon configures the RCON endpoint the operator probes for readiness and + // uses for graceful shutdown (spec §5, §7). + Rcon RconSpec `json:"rcon,omitempty"` + // Storage configures the world PVC. + Storage StorageSpec `json:"storage,omitempty"` + // Resources are the container resource requests/limits. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + // Lifecycle tunes graceful shutdown (spec §7). + Lifecycle LifecycleSpec `json:"lifecycle,omitempty"` + // Startup bounds how long Starting may last before Failed (spec §5). + Startup StartupSpec `json:"startup,omitempty"` + // Idle configures empty-server auto-stop (spec §8). + Idle IdleSpec `json:"idle,omitempty"` +} + +// EnvVar is a name/value pair injected into the server container. It is a +// deliberately narrow subset of corev1.EnvVar (no valueFrom) so the CRD cannot +// be used to exfiltrate arbitrary cluster secrets. +type EnvVar struct { + Name string `json:"name"` + Value string `json:"value,omitempty"` +} + +// MotdSpec holds per-phase MOTD strings (spec §4 spec.motd). +type MotdSpec struct { + Running string `json:"running,omitempty"` + Stopped string `json:"stopped,omitempty"` + Starting string `json:"starting,omitempty"` + Failed string `json:"failed,omitempty"` +} + +// RconSpec configures RCON (spec §4 spec.rcon). +type RconSpec struct { + // Enabled must be true for readiness probing and graceful shutdown. + Enabled bool `json:"enabled,omitempty"` + // Port is the RCON TCP port (default 25575). + Port int32 `json:"port,omitempty"` + // SecretRef points at the Secret holding the RCON password. + SecretRef SecretKeyRef `json:"secretRef,omitempty"` +} + +// SecretKeyRef references a single key within a Secret in the same namespace. +type SecretKeyRef struct { + Name string `json:"name"` + Key string `json:"key"` +} + +// StorageSpec configures the world PVC (spec §4 spec.storage). +type StorageSpec struct { + // Size is the requested PVC capacity (e.g. "10Gi"). + Size string `json:"size,omitempty"` + // StorageClassName selects the StorageClass; empty uses the default. + StorageClassName string `json:"storageClassName,omitempty"` + // RetainOnDelete keeps the PVC when the MinecraftServer is deleted. + RetainOnDelete bool `json:"retainOnDelete,omitempty"` +} + +// LifecycleSpec tunes graceful shutdown (spec §7). The operator injects a +// preStop RCON "save-all flush; stop" hook when PreStopSaveAndStop is set. +type LifecycleSpec struct { + // TerminationGracePeriodSeconds is the pod grace period (default 300). + TerminationGracePeriodSeconds int64 `json:"terminationGracePeriodSeconds,omitempty"` + // PreStopSaveAndStop enables the operator-injected RCON save+stop preStop. + PreStopSaveAndStop bool `json:"preStopSaveAndStop,omitempty"` +} + +// StartupSpec bounds the Starting phase (spec §5). +type StartupSpec struct { + // TimeoutSeconds is the overall budget before the server is marked Failed. + TimeoutSeconds int32 `json:"timeoutSeconds,omitempty"` + // ReadinessTimeoutSeconds is the budget for the first successful RCON probe. + ReadinessTimeoutSeconds int32 `json:"readinessTimeoutSeconds,omitempty"` +} + +// IdleSpec configures empty-server auto-stop (spec §8). +type IdleSpec struct { + // AutoStopEnabled turns on idle auto-stop. + AutoStopEnabled bool `json:"autoStopEnabled,omitempty"` + // EmptySecondsBeforeStop is how long the server may sit empty before the + // operator scales it down. + EmptySecondsBeforeStop int32 `json:"emptySecondsBeforeStop,omitempty"` +} + +// MinecraftServerStatus is the observed state (spec §4 status.*). +type MinecraftServerStatus struct { + // Phase is the coarse lifecycle phase. + Phase Phase `json:"phase,omitempty"` + // Ready is true only after a successful RCON probe (loader-agnostic; a + // status ping is never sufficient — spec §5). + Ready bool `json:"ready,omitempty"` + // Endpoint is where the proxy should route traffic. + Endpoint EndpointStatus `json:"endpoint,omitempty"` + // Players is the last observed player count. + Players PlayersStatus `json:"players,omitempty"` + // LiveMotd is the MOTD currently advertised for the active phase. + LiveMotd string `json:"liveMotd,omitempty"` + // ReadySignalAt is when the first RCON probe succeeded. + ReadySignalAt *metav1.Time `json:"readySignalAt,omitempty"` + // ObservedGeneration is the spec generation this status reflects. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Conditions are the standard metav1 conditions (Ready, RconReached, ...). + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// EndpointStatus is the resolved routing target (spec §4 status.endpoint). +type EndpointStatus struct { + // Mode is "direct" or "fallback". + Mode EndpointMode `json:"mode,omitempty"` + // Address is the host:port the proxy should dial. + Address string `json:"address,omitempty"` +} + +// PlayersStatus is the last observed player count (spec §4 status.players). +type PlayersStatus struct { + Online int32 `json:"online"` + Max int32 `json:"max"` +} diff --git a/internal/apis/felis/v1alpha1/zz_generated.deepcopy.go b/internal/apis/felis/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..b8ddaba --- /dev/null +++ b/internal/apis/felis/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,135 @@ +//go:build !ignore_autogenerated + +// Code in this file is hand-written to the same contract controller-gen would +// emit (DeepCopyInto/DeepCopy/DeepCopyObject) so the package needs no codegen +// toolchain to compile and satisfy runtime.Object. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto copies the receiver into out. +func (in *MinecraftServer) DeepCopyInto(out *MinecraftServer) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy returns a deep copy of the receiver. +func (in *MinecraftServer) DeepCopy() *MinecraftServer { + if in == nil { + return nil + } + out := new(MinecraftServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject implements runtime.Object. +func (in *MinecraftServer) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto copies the receiver into out. +func (in *MinecraftServerList) DeepCopyInto(out *MinecraftServerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + l := make([]MinecraftServer, len(in.Items)) + for i := range in.Items { + in.Items[i].DeepCopyInto(&l[i]) + } + out.Items = l + } +} + +// DeepCopy returns a deep copy of the receiver. +func (in *MinecraftServerList) DeepCopy() *MinecraftServerList { + if in == nil { + return nil + } + out := new(MinecraftServerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject implements runtime.Object. +func (in *MinecraftServerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto copies the receiver into out. +func (in *MinecraftServerSpec) DeepCopyInto(out *MinecraftServerSpec) { + *out = *in + if in.JavaFlags != nil { + l := make([]string, len(in.JavaFlags)) + copy(l, in.JavaFlags) + out.JavaFlags = l + } + if in.Args != nil { + l := make([]string, len(in.Args)) + copy(l, in.Args) + out.Args = l + } + if in.Env != nil { + l := make([]EnvVar, len(in.Env)) + copy(l, in.Env) + out.Env = l + } + out.Motd = in.Motd + out.Rcon = in.Rcon + out.Storage = in.Storage + in.Resources.DeepCopyInto(&out.Resources) + out.Lifecycle = in.Lifecycle + out.Startup = in.Startup + out.Idle = in.Idle +} + +// DeepCopy returns a deep copy of the receiver. +func (in *MinecraftServerSpec) DeepCopy() *MinecraftServerSpec { + if in == nil { + return nil + } + out := new(MinecraftServerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto copies the receiver into out. +func (in *MinecraftServerStatus) DeepCopyInto(out *MinecraftServerStatus) { + *out = *in + out.Endpoint = in.Endpoint + out.Players = in.Players + if in.ReadySignalAt != nil { + out.ReadySignalAt = in.ReadySignalAt.DeepCopy() + } + if in.Conditions != nil { + l := make([]metav1.Condition, len(in.Conditions)) + for i := range in.Conditions { + in.Conditions[i].DeepCopyInto(&l[i]) + } + out.Conditions = l + } +} + +// DeepCopy returns a deep copy of the receiver. +func (in *MinecraftServerStatus) DeepCopy() *MinecraftServerStatus { + if in == nil { + return nil + } + out := new(MinecraftServerStatus) + in.DeepCopyInto(out) + return out +}