mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 10:49:21 +00:00
✨ feat: add Go Kubernetes operator
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
bin/
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:1.26 AS build
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY cmd/ cmd/
|
||||||
|
COPY api/ api/
|
||||||
|
COPY internal/ internal/
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o operator ./cmd
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static:nonroot
|
||||||
|
WORKDIR /
|
||||||
|
COPY --from=build /workspace/operator .
|
||||||
|
USER 65532:65532
|
||||||
|
|
||||||
|
ENTRYPOINT ["/operator"]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
IMG ?= minikura-operator:latest
|
||||||
|
NAMESPACE ?= minikura
|
||||||
|
CONTROLLER_GEN ?= $(shell go env GOPATH)/bin/controller-gen
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: generate manifests fmt vet test build
|
||||||
|
|
||||||
|
.PHONY: controller-gen
|
||||||
|
controller-gen:
|
||||||
|
@test -x $(CONTROLLER_GEN) || go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest
|
||||||
|
|
||||||
|
.PHONY: generate
|
||||||
|
generate: controller-gen
|
||||||
|
$(CONTROLLER_GEN) object:headerFile=/dev/null paths=./api/...
|
||||||
|
|
||||||
|
.PHONY: manifests
|
||||||
|
manifests: controller-gen
|
||||||
|
$(CONTROLLER_GEN) crd paths=./api/... output:crd:artifacts:config=config/crd
|
||||||
|
$(CONTROLLER_GEN) rbac:roleName=minikura-operator-role paths=./internal/... output:rbac:artifacts:config=config/rbac
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test ./... -race
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
go build -o bin/operator ./cmd
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run:
|
||||||
|
go run ./cmd --leader-elect=false
|
||||||
|
|
||||||
|
.PHONY: docker-build
|
||||||
|
docker-build:
|
||||||
|
docker build -t $(IMG) .
|
||||||
|
|
||||||
|
.PHONY: install-crds
|
||||||
|
install-crds: manifests
|
||||||
|
kubectl apply -f config/crd
|
||||||
|
|
||||||
|
.PHONY: uninstall-crds
|
||||||
|
uninstall-crds:
|
||||||
|
kubectl delete --ignore-not-found -f config/crd
|
||||||
|
|
||||||
|
.PHONY: deploy
|
||||||
|
deploy: manifests
|
||||||
|
kubectl apply -f config/crd
|
||||||
|
kubectl apply -n $(NAMESPACE) -f config/rbac
|
||||||
|
kubectl apply -n $(NAMESPACE) -f config/manager
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
const (
|
||||||
|
Domain = "minikura.kirameki.cafe"
|
||||||
|
LabelPrefix = Domain
|
||||||
|
|
||||||
|
LabelServerType = LabelPrefix + "/server-type"
|
||||||
|
LabelServerID = LabelPrefix + "/server-id"
|
||||||
|
LabelProxyID = LabelPrefix + "/proxy-id"
|
||||||
|
LabelManagedBy = LabelPrefix + "/managed-by"
|
||||||
|
|
||||||
|
ManagerName = "minikura-operator"
|
||||||
|
|
||||||
|
PhasePending = "Pending"
|
||||||
|
PhaseRunning = "Running"
|
||||||
|
PhaseFailed = "Failed"
|
||||||
|
|
||||||
|
ConditionReady = "Ready"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EnvVar struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer
|
||||||
|
type ServiceExposure string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExposureClusterIP ServiceExposure = "ClusterIP"
|
||||||
|
ExposureNodePort ServiceExposure = "NodePort"
|
||||||
|
ExposureLoadBalancer ServiceExposure = "LoadBalancer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resources struct {
|
||||||
|
// +kubebuilder:validation:Minimum=256
|
||||||
|
// +kubebuilder:default=2048
|
||||||
|
MemoryLimitMB int32 `json:"memoryLimitMB,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Minimum=256
|
||||||
|
MemoryRequestMB int32 `json:"memoryRequestMB,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default="500m"
|
||||||
|
CPURequest string `json:"cpuRequest,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default="2"
|
||||||
|
CPULimit string `json:"cpuLimit,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// +kubebuilder:object:generate=true
|
||||||
|
// +groupName=minikura.kirameki.cafe
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GroupVersion = schema.GroupVersion{Group: "minikura.kirameki.cafe", Version: "v1alpha1"}
|
||||||
|
|
||||||
|
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||||
|
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=STATEFUL;STATELESS
|
||||||
|
type ServerKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServerStateful ServerKind = "STATEFUL"
|
||||||
|
ServerStateless ServerKind = "STATELESS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=VANILLA;PAPER;SPIGOT;PURPUR;FABRIC;FORGE;FOLIA
|
||||||
|
type JarType string
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=PEACEFUL;EASY;NORMAL;HARD
|
||||||
|
type Difficulty string
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=SURVIVAL;CREATIVE;ADVENTURE;SPECTATOR
|
||||||
|
type GameMode string
|
||||||
|
|
||||||
|
type MinecraftProperties struct {
|
||||||
|
// +kubebuilder:default=EASY
|
||||||
|
Difficulty Difficulty `json:"difficulty,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=SURVIVAL
|
||||||
|
GameMode GameMode `json:"gameMode,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:default=20
|
||||||
|
MaxPlayers int32 `json:"maxPlayers,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=true
|
||||||
|
PVP *bool `json:"pvp,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=true
|
||||||
|
OnlineMode *bool `json:"onlineMode,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
MOTD string `json:"motd,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
LevelSeed string `json:"levelSeed,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
LevelType string `json:"levelType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JVMOptions struct {
|
||||||
|
// +optional
|
||||||
|
Opts string `json:"opts,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
UseAikarFlags bool `json:"useAikarFlags,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
UseMeowIceFlags bool `json:"useMeowIceFlags,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:Maximum=100
|
||||||
|
// +kubebuilder:default=80
|
||||||
|
HeapPercent int32 `json:"heapPercent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MinecraftServerSpec struct {
|
||||||
|
Type ServerKind `json:"type"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:Maximum=65535
|
||||||
|
// +kubebuilder:default=25565
|
||||||
|
ListenPort int32 `json:"listenPort,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=ClusterIP
|
||||||
|
ServiceType ServiceExposure `json:"serviceType,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Minimum=30000
|
||||||
|
// +kubebuilder:validation:Maximum=32767
|
||||||
|
NodePort int32 `json:"nodePort,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=VANILLA
|
||||||
|
JarType JarType `json:"jarType,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default="LATEST"
|
||||||
|
MinecraftVersion string `json:"minecraftVersion,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Resources Resources `json:"resources,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
JVM JVMOptions `json:"jvm,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Properties MinecraftProperties `json:"properties,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Env []EnvVar `json:"env,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
APIKeySecretRef string `json:"apiKeySecretRef,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default="1Gi"
|
||||||
|
StorageSize string `json:"storageSize,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MinecraftServerStatus struct {
|
||||||
|
// +optional
|
||||||
|
Phase string `json:"phase,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Replicas int32 `json:"replicas,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +patchMergeKey=type
|
||||||
|
// +patchStrategy=merge
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=type
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:resource:shortName=mcs
|
||||||
|
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
|
||||||
|
// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=`.spec.minecraftVersion`
|
||||||
|
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||||
|
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.readyReplicas`
|
||||||
|
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.status.endpoint`
|
||||||
|
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||||
|
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
|
||||||
|
type MinecraftServerList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []MinecraftServer `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&MinecraftServer{}, &MinecraftServerList{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Enum=VELOCITY;BUNGEECORD
|
||||||
|
type ProxyKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProxyVelocity ProxyKind = "VELOCITY"
|
||||||
|
ProxyBungeeCord ProxyKind = "BUNGEECORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReverseProxyServerSpec struct {
|
||||||
|
Type ProxyKind `json:"type"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
|
||||||
|
ExternalAddress string `json:"externalAddress"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:Maximum=65535
|
||||||
|
ExternalPort int32 `json:"externalPort"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:Maximum=65535
|
||||||
|
// +kubebuilder:default=25565
|
||||||
|
ListenPort int32 `json:"listenPort,omitempty"`
|
||||||
|
|
||||||
|
// +kubebuilder:default=LoadBalancer
|
||||||
|
ServiceType ServiceExposure `json:"serviceType,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Minimum=30000
|
||||||
|
// +kubebuilder:validation:Maximum=32767
|
||||||
|
NodePort int32 `json:"nodePort,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Resources Resources `json:"resources,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
JVM JVMOptions `json:"jvm,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Env []EnvVar `json:"env,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
APIKeySecretRef string `json:"apiKeySecretRef,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
BackendSelector *metav1.LabelSelector `json:"backendSelector,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReverseProxyServerStatus struct {
|
||||||
|
// +optional
|
||||||
|
Phase string `json:"phase,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Replicas int32 `json:"replicas,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Backends []string `json:"backends,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +patchMergeKey=type
|
||||||
|
// +patchStrategy=merge
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=type
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:resource:shortName=rps
|
||||||
|
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
|
||||||
|
// +kubebuilder:printcolumn:name="Address",type=string,JSONPath=`.spec.externalAddress`
|
||||||
|
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||||
|
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.readyReplicas`
|
||||||
|
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||||
|
type ReverseProxyServer struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec ReverseProxyServerSpec `json:"spec,omitempty"`
|
||||||
|
Status ReverseProxyServerStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
type ReverseProxyServerList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []ReverseProxyServer `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&ReverseProxyServer{}, &ReverseProxyServerList{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
//go:build !ignore_autogenerated
|
||||||
|
|
||||||
|
// Code generated by controller-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EnvVar) DeepCopyInto(out *EnvVar) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvVar.
|
||||||
|
func (in *EnvVar) DeepCopy() *EnvVar {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EnvVar)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *JVMOptions) DeepCopyInto(out *JVMOptions) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JVMOptions.
|
||||||
|
func (in *JVMOptions) DeepCopy() *JVMOptions {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(JVMOptions)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MinecraftProperties) DeepCopyInto(out *MinecraftProperties) {
|
||||||
|
*out = *in
|
||||||
|
if in.PVP != nil {
|
||||||
|
in, out := &in.PVP, &out.PVP
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.OnlineMode != nil {
|
||||||
|
in, out := &in.OnlineMode, &out.OnlineMode
|
||||||
|
*out = new(bool)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MinecraftProperties.
|
||||||
|
func (in *MinecraftProperties) DeepCopy() *MinecraftProperties {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MinecraftProperties)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
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 is an autogenerated deepcopy function, copying the receiver, creating a new MinecraftServer.
|
||||||
|
func (in *MinecraftServer) DeepCopy() *MinecraftServer {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MinecraftServer)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *MinecraftServer) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MinecraftServerList) DeepCopyInto(out *MinecraftServerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]MinecraftServer, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MinecraftServerList.
|
||||||
|
func (in *MinecraftServerList) DeepCopy() *MinecraftServerList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MinecraftServerList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *MinecraftServerList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MinecraftServerSpec) DeepCopyInto(out *MinecraftServerSpec) {
|
||||||
|
*out = *in
|
||||||
|
out.Resources = in.Resources
|
||||||
|
out.JVM = in.JVM
|
||||||
|
in.Properties.DeepCopyInto(&out.Properties)
|
||||||
|
if in.Env != nil {
|
||||||
|
in, out := &in.Env, &out.Env
|
||||||
|
*out = make([]EnvVar, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MinecraftServerSpec.
|
||||||
|
func (in *MinecraftServerSpec) DeepCopy() *MinecraftServerSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MinecraftServerSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MinecraftServerStatus) DeepCopyInto(out *MinecraftServerStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.Conditions != nil {
|
||||||
|
in, out := &in.Conditions, &out.Conditions
|
||||||
|
*out = make([]v1.Condition, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MinecraftServerStatus.
|
||||||
|
func (in *MinecraftServerStatus) DeepCopy() *MinecraftServerStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MinecraftServerStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Resources) DeepCopyInto(out *Resources) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources.
|
||||||
|
func (in *Resources) DeepCopy() *Resources {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Resources)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ReverseProxyServer) DeepCopyInto(out *ReverseProxyServer) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReverseProxyServer.
|
||||||
|
func (in *ReverseProxyServer) DeepCopy() *ReverseProxyServer {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ReverseProxyServer)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *ReverseProxyServer) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ReverseProxyServerList) DeepCopyInto(out *ReverseProxyServerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]ReverseProxyServer, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReverseProxyServerList.
|
||||||
|
func (in *ReverseProxyServerList) DeepCopy() *ReverseProxyServerList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ReverseProxyServerList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *ReverseProxyServerList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ReverseProxyServerSpec) DeepCopyInto(out *ReverseProxyServerSpec) {
|
||||||
|
*out = *in
|
||||||
|
out.Resources = in.Resources
|
||||||
|
out.JVM = in.JVM
|
||||||
|
if in.Env != nil {
|
||||||
|
in, out := &in.Env, &out.Env
|
||||||
|
*out = make([]EnvVar, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.BackendSelector != nil {
|
||||||
|
in, out := &in.BackendSelector, &out.BackendSelector
|
||||||
|
*out = new(v1.LabelSelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReverseProxyServerSpec.
|
||||||
|
func (in *ReverseProxyServerSpec) DeepCopy() *ReverseProxyServerSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ReverseProxyServerSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ReverseProxyServerStatus) DeepCopyInto(out *ReverseProxyServerStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.Backends != nil {
|
||||||
|
in, out := &in.Backends, &out.Backends
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.Conditions != nil {
|
||||||
|
in, out := &in.Conditions, &out.Conditions
|
||||||
|
*out = make([]v1.Condition, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReverseProxyServerStatus.
|
||||||
|
func (in *ReverseProxyServerStatus) DeepCopy() *ReverseProxyServerStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ReverseProxyServerStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
"github.com/YuzuZensai/Minikura/operator/internal/controller"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scheme = runtime.NewScheme()
|
||||||
|
setupLog = ctrl.Log.WithName("setup")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(v1alpha1.AddToScheme(scheme))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var metricsAddr, probeAddr string
|
||||||
|
var enableLeaderElection bool
|
||||||
|
|
||||||
|
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "Address the metric endpoint binds to.")
|
||||||
|
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "Address the probe endpoint binds to.")
|
||||||
|
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||||
|
"Enable leader election, ensuring only one active operator when running multiple replicas.")
|
||||||
|
|
||||||
|
opts := zap.Options{Development: os.Getenv("NODE_ENV") != "production"}
|
||||||
|
opts.BindFlags(flag.CommandLine)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||||
|
|
||||||
|
namespace := os.Getenv("KUBERNETES_NAMESPACE")
|
||||||
|
if namespace == "" {
|
||||||
|
namespace = "minikura"
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
Scheme: scheme,
|
||||||
|
Metrics: metricsserver.Options{BindAddress: metricsAddr},
|
||||||
|
HealthProbeBindAddress: probeAddr,
|
||||||
|
LeaderElection: enableLeaderElection,
|
||||||
|
LeaderElectionID: "minikura-operator.minikura.kirameki.cafe",
|
||||||
|
Cache: cache.Options{
|
||||||
|
DefaultNamespaces: map[string]cache.Config{namespace: {}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
setupLog.Error(err, "unable to start manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (&controller.MinecraftServerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create controller", "controller", "MinecraftServer")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (&controller.ReverseProxyServerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create controller", "controller", "ReverseProxyServer")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
setupLog.Error(err, "unable to set up health check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||||
|
setupLog.Error(err, "unable to set up ready check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLog.Info("starting Minikura operator", "namespace", namespace)
|
||||||
|
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||||
|
setupLog.Error(err, "problem running manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.21.0
|
||||||
|
name: minecraftservers.minikura.kirameki.cafe
|
||||||
|
spec:
|
||||||
|
group: minikura.kirameki.cafe
|
||||||
|
names:
|
||||||
|
kind: MinecraftServer
|
||||||
|
listKind: MinecraftServerList
|
||||||
|
plural: minecraftservers
|
||||||
|
shortNames:
|
||||||
|
- mcs
|
||||||
|
singular: minecraftserver
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.type
|
||||||
|
name: Type
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.minecraftVersion
|
||||||
|
name: Version
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.readyReplicas
|
||||||
|
name: Ready
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.endpoint
|
||||||
|
name: Endpoint
|
||||||
|
type: string
|
||||||
|
- jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
properties:
|
||||||
|
apiKeySecretRef:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
jarType:
|
||||||
|
default: VANILLA
|
||||||
|
enum:
|
||||||
|
- VANILLA
|
||||||
|
- PAPER
|
||||||
|
- SPIGOT
|
||||||
|
- PURPUR
|
||||||
|
- FABRIC
|
||||||
|
- FORGE
|
||||||
|
- FOLIA
|
||||||
|
type: string
|
||||||
|
jvm:
|
||||||
|
properties:
|
||||||
|
heapPercent:
|
||||||
|
default: 80
|
||||||
|
format: int32
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
opts:
|
||||||
|
type: string
|
||||||
|
useAikarFlags:
|
||||||
|
type: boolean
|
||||||
|
useMeowIceFlags:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
listenPort:
|
||||||
|
default: 25565
|
||||||
|
format: int32
|
||||||
|
maximum: 65535
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
minecraftVersion:
|
||||||
|
default: LATEST
|
||||||
|
type: string
|
||||||
|
nodePort:
|
||||||
|
format: int32
|
||||||
|
maximum: 32767
|
||||||
|
minimum: 30000
|
||||||
|
type: integer
|
||||||
|
properties:
|
||||||
|
properties:
|
||||||
|
difficulty:
|
||||||
|
default: EASY
|
||||||
|
enum:
|
||||||
|
- PEACEFUL
|
||||||
|
- EASY
|
||||||
|
- NORMAL
|
||||||
|
- HARD
|
||||||
|
type: string
|
||||||
|
gameMode:
|
||||||
|
default: SURVIVAL
|
||||||
|
enum:
|
||||||
|
- SURVIVAL
|
||||||
|
- CREATIVE
|
||||||
|
- ADVENTURE
|
||||||
|
- SPECTATOR
|
||||||
|
type: string
|
||||||
|
levelSeed:
|
||||||
|
type: string
|
||||||
|
levelType:
|
||||||
|
type: string
|
||||||
|
maxPlayers:
|
||||||
|
default: 20
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
motd:
|
||||||
|
type: string
|
||||||
|
onlineMode:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
pvp:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
resources:
|
||||||
|
properties:
|
||||||
|
cpuLimit:
|
||||||
|
default: "2"
|
||||||
|
type: string
|
||||||
|
cpuRequest:
|
||||||
|
default: 500m
|
||||||
|
type: string
|
||||||
|
memoryLimitMB:
|
||||||
|
default: 2048
|
||||||
|
format: int32
|
||||||
|
minimum: 256
|
||||||
|
type: integer
|
||||||
|
memoryRequestMB:
|
||||||
|
format: int32
|
||||||
|
minimum: 256
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
serviceType:
|
||||||
|
default: ClusterIP
|
||||||
|
enum:
|
||||||
|
- ClusterIP
|
||||||
|
- NodePort
|
||||||
|
- LoadBalancer
|
||||||
|
type: string
|
||||||
|
storageSize:
|
||||||
|
default: 1Gi
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- STATEFUL
|
||||||
|
- STATELESS
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
phase:
|
||||||
|
type: string
|
||||||
|
readyReplicas:
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
replicas:
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.21.0
|
||||||
|
name: reverseproxyservers.minikura.kirameki.cafe
|
||||||
|
spec:
|
||||||
|
group: minikura.kirameki.cafe
|
||||||
|
names:
|
||||||
|
kind: ReverseProxyServer
|
||||||
|
listKind: ReverseProxyServerList
|
||||||
|
plural: reverseproxyservers
|
||||||
|
shortNames:
|
||||||
|
- rps
|
||||||
|
singular: reverseproxyserver
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.type
|
||||||
|
name: Type
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.externalAddress
|
||||||
|
name: Address
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.readyReplicas
|
||||||
|
name: Ready
|
||||||
|
type: string
|
||||||
|
- jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
properties:
|
||||||
|
apiKeySecretRef:
|
||||||
|
type: string
|
||||||
|
backendSelector:
|
||||||
|
description: |-
|
||||||
|
A label selector is a label query over a set of resources. The result of matchLabels and
|
||||||
|
matchExpressions are ANDed. An empty label selector matches all objects. A null
|
||||||
|
label selector matches no objects.
|
||||||
|
properties:
|
||||||
|
matchExpressions:
|
||||||
|
description: matchExpressions is a list of label selector requirements.
|
||||||
|
The requirements are ANDed.
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
A label selector requirement is a selector that contains values, a key, and an operator that
|
||||||
|
relates the key and values.
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: key is the label key that the selector applies
|
||||||
|
to.
|
||||||
|
type: string
|
||||||
|
operator:
|
||||||
|
description: |-
|
||||||
|
operator represents a key's relationship to a set of values.
|
||||||
|
Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||||
|
type: string
|
||||||
|
values:
|
||||||
|
description: |-
|
||||||
|
values is an array of string values. If the operator is In or NotIn,
|
||||||
|
the values array must be non-empty. If the operator is Exists or DoesNotExist,
|
||||||
|
the values array must be empty. This array is replaced during a strategic
|
||||||
|
merge patch.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
required:
|
||||||
|
- key
|
||||||
|
- operator
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
matchLabels:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
|
||||||
|
map is equivalent to an element of matchExpressions, whose key field is "key", the
|
||||||
|
operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
externalAddress:
|
||||||
|
type: string
|
||||||
|
externalPort:
|
||||||
|
format: int32
|
||||||
|
maximum: 65535
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
jvm:
|
||||||
|
properties:
|
||||||
|
heapPercent:
|
||||||
|
default: 80
|
||||||
|
format: int32
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
opts:
|
||||||
|
type: string
|
||||||
|
useAikarFlags:
|
||||||
|
type: boolean
|
||||||
|
useMeowIceFlags:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
listenPort:
|
||||||
|
default: 25565
|
||||||
|
format: int32
|
||||||
|
maximum: 65535
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
nodePort:
|
||||||
|
format: int32
|
||||||
|
maximum: 32767
|
||||||
|
minimum: 30000
|
||||||
|
type: integer
|
||||||
|
resources:
|
||||||
|
properties:
|
||||||
|
cpuLimit:
|
||||||
|
default: "2"
|
||||||
|
type: string
|
||||||
|
cpuRequest:
|
||||||
|
default: 500m
|
||||||
|
type: string
|
||||||
|
memoryLimitMB:
|
||||||
|
default: 2048
|
||||||
|
format: int32
|
||||||
|
minimum: 256
|
||||||
|
type: integer
|
||||||
|
memoryRequestMB:
|
||||||
|
format: int32
|
||||||
|
minimum: 256
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
serviceType:
|
||||||
|
default: LoadBalancer
|
||||||
|
enum:
|
||||||
|
- ClusterIP
|
||||||
|
- NodePort
|
||||||
|
- LoadBalancer
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- VELOCITY
|
||||||
|
- BUNGEECORD
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- externalAddress
|
||||||
|
- externalPort
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
properties:
|
||||||
|
backends:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
phase:
|
||||||
|
type: string
|
||||||
|
readyReplicas:
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
replicas:
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: minikura-operator
|
||||||
|
labels:
|
||||||
|
app: minikura-operator
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: minikura-operator
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: minikura-operator
|
||||||
|
spec:
|
||||||
|
serviceAccountName: minikura-operator
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: operator
|
||||||
|
image: minikura-operator:latest
|
||||||
|
args:
|
||||||
|
- --leader-elect
|
||||||
|
env:
|
||||||
|
- name: KUBERNETES_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: production
|
||||||
|
ports:
|
||||||
|
- name: metrics
|
||||||
|
containerPort: 8080
|
||||||
|
- name: health
|
||||||
|
containerPort: 8081
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: health
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: health
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
terminationGracePeriodSeconds: 10
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: minikura-operator-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- deployments
|
||||||
|
- statefulsets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- coordination.k8s.io
|
||||||
|
resources:
|
||||||
|
- leases
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- minikura.kirameki.cafe
|
||||||
|
resources:
|
||||||
|
- minecraftservers
|
||||||
|
- reverseproxyservers
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- minikura.kirameki.cafe
|
||||||
|
resources:
|
||||||
|
- minecraftservers/finalizers
|
||||||
|
- reverseproxyservers/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- minikura.kirameki.cafe
|
||||||
|
resources:
|
||||||
|
- minecraftservers/status
|
||||||
|
- reverseproxyservers/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: minikura-operator
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: minikura-operator-rolebinding
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: minikura-operator-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: minikura-operator
|
||||||
|
namespace: minikura
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
apiVersion: minikura.kirameki.cafe/v1alpha1
|
||||||
|
kind: MinecraftServer
|
||||||
|
metadata:
|
||||||
|
name: survival
|
||||||
|
spec:
|
||||||
|
type: STATEFUL
|
||||||
|
description: Example SMP server
|
||||||
|
jarType: PAPER
|
||||||
|
minecraftVersion: "1.20.4"
|
||||||
|
listenPort: 25565
|
||||||
|
serviceType: ClusterIP
|
||||||
|
storageSize: 5Gi
|
||||||
|
resources:
|
||||||
|
memoryLimitMB: 4096
|
||||||
|
memoryRequestMB: 2048
|
||||||
|
cpuRequest: 500m
|
||||||
|
cpuLimit: "2"
|
||||||
|
jvm:
|
||||||
|
useAikarFlags: true
|
||||||
|
heapPercent: 80
|
||||||
|
properties:
|
||||||
|
difficulty: HARD
|
||||||
|
gameMode: SURVIVAL
|
||||||
|
maxPlayers: 20
|
||||||
|
pvp: true
|
||||||
|
onlineMode: true
|
||||||
|
motd: A Minikura server
|
||||||
|
---
|
||||||
|
apiVersion: minikura.kirameki.cafe/v1alpha1
|
||||||
|
kind: MinecraftServer
|
||||||
|
metadata:
|
||||||
|
name: lobby
|
||||||
|
labels:
|
||||||
|
role: lobby
|
||||||
|
spec:
|
||||||
|
type: STATELESS
|
||||||
|
jarType: PAPER
|
||||||
|
minecraftVersion: "1.20.4"
|
||||||
|
serviceType: ClusterIP
|
||||||
|
resources:
|
||||||
|
memoryLimitMB: 2048
|
||||||
|
properties:
|
||||||
|
gameMode: ADVENTURE
|
||||||
|
maxPlayers: 100
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: minikura.kirameki.cafe/v1alpha1
|
||||||
|
kind: ReverseProxyServer
|
||||||
|
metadata:
|
||||||
|
name: edge
|
||||||
|
spec:
|
||||||
|
type: VELOCITY
|
||||||
|
description: Example Velocity proxy
|
||||||
|
externalAddress: play.example.com
|
||||||
|
externalPort: 25565
|
||||||
|
listenPort: 25577
|
||||||
|
serviceType: LoadBalancer
|
||||||
|
resources:
|
||||||
|
memoryLimitMB: 1024
|
||||||
|
cpuRequest: 250m
|
||||||
|
cpuLimit: "1"
|
||||||
|
jvm:
|
||||||
|
heapPercent: 80
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
module github.com/YuzuZensai/Minikura/operator
|
||||||
|
|
||||||
|
go 1.26.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
k8s.io/api v0.36.0
|
||||||
|
k8s.io/apimachinery v0.36.0
|
||||||
|
k8s.io/client-go v0.36.0
|
||||||
|
sigs.k8s.io/controller-runtime v0.24.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/zapr v1.3.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
golang.org/x/term v0.39.0 // indirect
|
||||||
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/apiextensions-apiserver v0.36.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
|
)
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||||
|
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
|
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
|
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
|
||||||
|
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
||||||
|
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
||||||
|
k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0=
|
||||||
|
k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug=
|
||||||
|
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||||
|
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
||||||
|
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
|
||||||
|
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
|
||||||
|
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||||
|
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||||
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
|
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
|
||||||
|
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var applyOpts = []client.PatchOption{
|
||||||
|
client.FieldOwner(v1alpha1.ManagerName),
|
||||||
|
client.ForceOwnership,
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(ctx context.Context, c client.Client, owner client.Object, obj client.Object, scheme *runtime.Scheme) error {
|
||||||
|
if err := setOwner(owner, obj, scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gvk, err := apiutil.GVKForObject(obj, scheme)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.GetObjectKind().SetGroupVersionKind(gvk)
|
||||||
|
|
||||||
|
obj.SetManagedFields(nil)
|
||||||
|
obj.SetResourceVersion("")
|
||||||
|
return c.Patch(ctx, obj, client.Apply, applyOpts...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testScheme(t *testing.T) *runtime.Scheme {
|
||||||
|
t.Helper()
|
||||||
|
s := runtime.NewScheme()
|
||||||
|
if err := clientgoscheme.AddToScheme(s); err != nil {
|
||||||
|
t.Fatalf("add client-go scheme: %v", err)
|
||||||
|
}
|
||||||
|
if err := v1alpha1.AddToScheme(s); err != nil {
|
||||||
|
t.Fatalf("add v1alpha1 scheme: %v", err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypedObjectMarshalsWithoutTypeMeta(t *testing.T) {
|
||||||
|
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "b"}}
|
||||||
|
|
||||||
|
var m map[string]any
|
||||||
|
raw, err := json.Marshal(cm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &m); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m["apiVersion"]; ok {
|
||||||
|
t.Fatal("precondition changed: typed objects now carry apiVersion")
|
||||||
|
}
|
||||||
|
if _, ok := m["kind"]; ok {
|
||||||
|
t.Fatal("precondition changed: typed objects now carry kind")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplySetsTypeMeta(t *testing.T) {
|
||||||
|
scheme := testScheme(t)
|
||||||
|
owner := &v1alpha1.MinecraftServer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "smp", Namespace: "minikura", UID: "abc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "minikura"}}
|
||||||
|
if err := setOwner(owner, cm, scheme); err != nil {
|
||||||
|
t.Fatalf("setOwner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gvk, err := apiutil.GVKForObject(cm, scheme)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GVKForObject: %v", err)
|
||||||
|
}
|
||||||
|
cm.GetObjectKind().SetGroupVersionKind(gvk)
|
||||||
|
|
||||||
|
raw, err := json.Marshal(cm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &m); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m["apiVersion"] != "v1" {
|
||||||
|
t.Errorf("apiVersion = %v, want v1", m["apiVersion"])
|
||||||
|
}
|
||||||
|
if m["kind"] != "ConfigMap" {
|
||||||
|
t.Errorf("kind = %v, want ConfigMap", m["kind"])
|
||||||
|
}
|
||||||
|
|
||||||
|
refs := cm.GetOwnerReferences()
|
||||||
|
if len(refs) != 1 {
|
||||||
|
t.Fatalf("ownerReferences = %d, want 1", len(refs))
|
||||||
|
}
|
||||||
|
if refs[0].Kind != "MinecraftServer" || refs[0].Name != "smp" {
|
||||||
|
t.Errorf("owner = %s/%s, want MinecraftServer/smp", refs[0].Kind, refs[0].Name)
|
||||||
|
}
|
||||||
|
if refs[0].Controller == nil || !*refs[0].Controller {
|
||||||
|
t.Error("owner reference should be a controller reference")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
"github.com/YuzuZensai/Minikura/operator/internal/resources"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MinecraftServerReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=minikura.kirameki.cafe,resources=minecraftservers,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=minikura.kirameki.cafe,resources=minecraftservers/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=minikura.kirameki.cafe,resources=minecraftservers/finalizers,verbs=update
|
||||||
|
// +kubebuilder:rbac:groups=apps,resources=deployments;statefulsets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=services;configmaps,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
|
||||||
|
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var mc v1alpha1.MinecraftServer
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &mc); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mc.DeletionTimestamp.IsZero() {
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apply(ctx, r.Client, &mc, resources.MinecraftConfigMap(&mc), r.Scheme); err != nil {
|
||||||
|
return r.fail(ctx, &mc, "ConfigMapFailed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apply(ctx, r.Client, &mc, resources.MinecraftService(&mc), r.Scheme); err != nil {
|
||||||
|
return r.fail(ctx, &mc, "ServiceFailed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateful := mc.Spec.Type == v1alpha1.ServerStateful
|
||||||
|
if err := r.reconcileWorkload(ctx, &mc, stateful); err != nil {
|
||||||
|
return r.fail(ctx, &mc, "WorkloadFailed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.pruneOppositeWorkload(ctx, &mc, stateful); err != nil {
|
||||||
|
logger.Error(err, "failed to prune previous workload")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, r.updateStatus(ctx, &mc, stateful)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) reconcileWorkload(ctx context.Context, mc *v1alpha1.MinecraftServer, stateful bool) error {
|
||||||
|
if !stateful {
|
||||||
|
return apply(ctx, r.Client, mc, resources.MinecraftDeployment(mc), r.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
sts, err := resources.MinecraftStatefulSet(mc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return apply(ctx, r.Client, mc, sts, r.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) pruneOppositeWorkload(ctx context.Context, mc *v1alpha1.MinecraftServer, stateful bool) error {
|
||||||
|
name := resources.ServerName(mc.Name)
|
||||||
|
key := client.ObjectKey{Name: name, Namespace: mc.Namespace}
|
||||||
|
|
||||||
|
var stale client.Object
|
||||||
|
if stateful {
|
||||||
|
stale = &appsv1.Deployment{}
|
||||||
|
} else {
|
||||||
|
stale = &appsv1.StatefulSet{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Get(ctx, key, stale); err != nil {
|
||||||
|
return client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
if !metav1.IsControlledBy(stale, mc) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client.IgnoreNotFound(r.Delete(ctx, stale))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) updateStatus(ctx context.Context, mc *v1alpha1.MinecraftServer, stateful bool) error {
|
||||||
|
name := resources.ServerName(mc.Name)
|
||||||
|
key := client.ObjectKey{Name: name, Namespace: mc.Namespace}
|
||||||
|
|
||||||
|
var ready, replicas int32
|
||||||
|
if stateful {
|
||||||
|
var sts appsv1.StatefulSet
|
||||||
|
if err := r.Get(ctx, key, &sts); err == nil {
|
||||||
|
ready, replicas = sts.Status.ReadyReplicas, sts.Status.Replicas
|
||||||
|
} else if !apierrors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var dep appsv1.Deployment
|
||||||
|
if err := r.Get(ctx, key, &dep); err == nil {
|
||||||
|
ready, replicas = dep.Status.ReadyReplicas, dep.Status.Replicas
|
||||||
|
} else if !apierrors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
phase := v1alpha1.PhasePending
|
||||||
|
if ready > 0 {
|
||||||
|
phase = v1alpha1.PhaseRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := r.endpoint(ctx, mc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := client.MergeFrom(mc.DeepCopy())
|
||||||
|
mc.Status.Phase = phase
|
||||||
|
mc.Status.ReadyReplicas = ready
|
||||||
|
mc.Status.Replicas = replicas
|
||||||
|
mc.Status.Endpoint = endpoint
|
||||||
|
mc.Status.ObservedGeneration = mc.Generation
|
||||||
|
mc.Status.Message = ""
|
||||||
|
setCondition(&mc.Status.Conditions, metav1.Condition{
|
||||||
|
Type: v1alpha1.ConditionReady,
|
||||||
|
Status: conditionStatus(ready > 0),
|
||||||
|
Reason: phase,
|
||||||
|
ObservedGeneration: mc.Generation,
|
||||||
|
})
|
||||||
|
|
||||||
|
return r.Status().Patch(ctx, mc, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) endpoint(ctx context.Context, mc *v1alpha1.MinecraftServer) (string, error) {
|
||||||
|
var svc corev1.Service
|
||||||
|
key := client.ObjectKey{Name: resources.ServerName(mc.Name), Namespace: mc.Namespace}
|
||||||
|
if err := r.Get(ctx, key, &svc); err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return serviceEndpoint(&svc, mc.Namespace), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) fail(ctx context.Context, mc *v1alpha1.MinecraftServer, reason string, cause error) (ctrl.Result, error) {
|
||||||
|
patch := client.MergeFrom(mc.DeepCopy())
|
||||||
|
mc.Status.Phase = v1alpha1.PhaseFailed
|
||||||
|
mc.Status.Message = cause.Error()
|
||||||
|
setCondition(&mc.Status.Conditions, metav1.Condition{
|
||||||
|
Type: v1alpha1.ConditionReady,
|
||||||
|
Status: metav1.ConditionFalse,
|
||||||
|
Reason: reason,
|
||||||
|
Message: cause.Error(),
|
||||||
|
ObservedGeneration: mc.Generation,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := r.Status().Patch(ctx, mc, patch); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("%w (status patch failed: %v)", cause, err)
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MinecraftServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.MinecraftServer{}).
|
||||||
|
Owns(&appsv1.Deployment{}).
|
||||||
|
Owns(&appsv1.StatefulSet{}).
|
||||||
|
Owns(&corev1.Service{}).
|
||||||
|
Owns(&corev1.ConfigMap{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setOwner(owner, obj client.Object, scheme *runtime.Scheme) error {
|
||||||
|
return controllerutil.SetControllerReference(owner, obj, scheme)
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
"github.com/YuzuZensai/Minikura/operator/internal/resources"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReverseProxyServerReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=minikura.kirameki.cafe,resources=reverseproxyservers,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=minikura.kirameki.cafe,resources=reverseproxyservers/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=minikura.kirameki.cafe,resources=reverseproxyservers/finalizers,verbs=update
|
||||||
|
|
||||||
|
func (r *ReverseProxyServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
var rp v1alpha1.ReverseProxyServer
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &rp); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rp.DeletionTimestamp.IsZero() {
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apply(ctx, r.Client, &rp, resources.ProxyConfigMap(&rp), r.Scheme); err != nil {
|
||||||
|
return r.fail(ctx, &rp, "ConfigMapFailed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apply(ctx, r.Client, &rp, resources.ProxyService(&rp), r.Scheme); err != nil {
|
||||||
|
return r.fail(ctx, &rp, "ServiceFailed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apply(ctx, r.Client, &rp, resources.ProxyDeployment(&rp), r.Scheme); err != nil {
|
||||||
|
return r.fail(ctx, &rp, "DeploymentFailed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, r.updateStatus(ctx, &rp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReverseProxyServerReconciler) backends(ctx context.Context, rp *v1alpha1.ReverseProxyServer) ([]string, error) {
|
||||||
|
selector := labels.Everything()
|
||||||
|
if rp.Spec.BackendSelector != nil {
|
||||||
|
s, err := metav1.LabelSelectorAsSelector(rp.Spec.BackendSelector)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid backendSelector: %w", err)
|
||||||
|
}
|
||||||
|
selector = s
|
||||||
|
}
|
||||||
|
|
||||||
|
var list v1alpha1.MinecraftServerList
|
||||||
|
if err := r.List(ctx, &list,
|
||||||
|
client.InNamespace(rp.Namespace),
|
||||||
|
client.MatchingLabelsSelector{Selector: selector},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(list.Items))
|
||||||
|
for _, mc := range list.Items {
|
||||||
|
names = append(names, mc.Name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReverseProxyServerReconciler) updateStatus(ctx context.Context, rp *v1alpha1.ReverseProxyServer) error {
|
||||||
|
name := resources.ProxyName(rp.Spec.Type, rp.Name)
|
||||||
|
key := client.ObjectKey{Name: name, Namespace: rp.Namespace}
|
||||||
|
|
||||||
|
var ready, replicas int32
|
||||||
|
var dep appsv1.Deployment
|
||||||
|
if err := r.Get(ctx, key, &dep); err == nil {
|
||||||
|
ready, replicas = dep.Status.ReadyReplicas, dep.Status.Replicas
|
||||||
|
} else if !apierrors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
backends, err := r.backends(ctx, rp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := ""
|
||||||
|
var svc corev1.Service
|
||||||
|
if err := r.Get(ctx, key, &svc); err == nil {
|
||||||
|
endpoint = serviceEndpoint(&svc, rp.Namespace)
|
||||||
|
} else if !apierrors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
phase := v1alpha1.PhasePending
|
||||||
|
if ready > 0 {
|
||||||
|
phase = v1alpha1.PhaseRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := client.MergeFrom(rp.DeepCopy())
|
||||||
|
rp.Status.Phase = phase
|
||||||
|
rp.Status.ReadyReplicas = ready
|
||||||
|
rp.Status.Replicas = replicas
|
||||||
|
rp.Status.Endpoint = endpoint
|
||||||
|
rp.Status.Backends = backends
|
||||||
|
rp.Status.ObservedGeneration = rp.Generation
|
||||||
|
rp.Status.Message = ""
|
||||||
|
setCondition(&rp.Status.Conditions, metav1.Condition{
|
||||||
|
Type: v1alpha1.ConditionReady,
|
||||||
|
Status: conditionStatus(ready > 0),
|
||||||
|
Reason: phase,
|
||||||
|
ObservedGeneration: rp.Generation,
|
||||||
|
})
|
||||||
|
|
||||||
|
return r.Status().Patch(ctx, rp, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReverseProxyServerReconciler) fail(ctx context.Context, rp *v1alpha1.ReverseProxyServer, reason string, cause error) (ctrl.Result, error) {
|
||||||
|
patch := client.MergeFrom(rp.DeepCopy())
|
||||||
|
rp.Status.Phase = v1alpha1.PhaseFailed
|
||||||
|
rp.Status.Message = cause.Error()
|
||||||
|
setCondition(&rp.Status.Conditions, metav1.Condition{
|
||||||
|
Type: v1alpha1.ConditionReady,
|
||||||
|
Status: metav1.ConditionFalse,
|
||||||
|
Reason: reason,
|
||||||
|
Message: cause.Error(),
|
||||||
|
ObservedGeneration: rp.Generation,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := r.Status().Patch(ctx, rp, patch); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("%w (status patch failed: %v)", cause, err)
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReverseProxyServerReconciler) proxiesForServer(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||||
|
var list v1alpha1.ReverseProxyServerList
|
||||||
|
if err := r.List(ctx, &list, client.InNamespace(obj.GetNamespace())); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reqs := make([]reconcile.Request, 0, len(list.Items))
|
||||||
|
for _, rp := range list.Items {
|
||||||
|
reqs = append(reqs, reconcile.Request{
|
||||||
|
NamespacedName: client.ObjectKey{Name: rp.Name, Namespace: rp.Namespace},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return reqs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReverseProxyServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.ReverseProxyServer{}).
|
||||||
|
Owns(&appsv1.Deployment{}).
|
||||||
|
Owns(&corev1.Service{}).
|
||||||
|
Owns(&corev1.ConfigMap{}).
|
||||||
|
Watches(&v1alpha1.MinecraftServer{}, handler.EnqueueRequestsFromMapFunc(r.proxiesForServer)).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func conditionStatus(ok bool) metav1.ConditionStatus {
|
||||||
|
if ok {
|
||||||
|
return metav1.ConditionTrue
|
||||||
|
}
|
||||||
|
return metav1.ConditionFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCondition(conditions *[]metav1.Condition, c metav1.Condition) {
|
||||||
|
if c.Reason == "" {
|
||||||
|
c.Reason = "Unknown"
|
||||||
|
}
|
||||||
|
meta.SetStatusCondition(conditions, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceEndpoint(svc *corev1.Service, namespace string) string {
|
||||||
|
internal := fmt.Sprintf("%s.%s.svc.cluster.local", svc.Name, namespace)
|
||||||
|
port := int32(0)
|
||||||
|
if len(svc.Spec.Ports) > 0 {
|
||||||
|
port = svc.Spec.Ports[0].Port
|
||||||
|
}
|
||||||
|
|
||||||
|
switch svc.Spec.Type {
|
||||||
|
case corev1.ServiceTypeLoadBalancer:
|
||||||
|
for _, ing := range svc.Status.LoadBalancer.Ingress {
|
||||||
|
if host := ing.Hostname; host != "" {
|
||||||
|
return fmt.Sprintf("%s:%d", host, port)
|
||||||
|
}
|
||||||
|
if ip := ing.IP; ip != "" {
|
||||||
|
return fmt.Sprintf("%s:%d", ip, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
|
||||||
|
case corev1.ServiceTypeNodePort:
|
||||||
|
if len(svc.Spec.Ports) > 0 && svc.Spec.Ports[0].NodePort != 0 {
|
||||||
|
return fmt.Sprintf("<node-ip>:%d", svc.Spec.Ports[0].NodePort)
|
||||||
|
}
|
||||||
|
return internal
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s:%d", internal, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MinecraftImage = "itzg/minecraft-server"
|
||||||
|
ContainerPort = 25565
|
||||||
|
DefaultHeapPercent = 80
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServiceType(exposure v1alpha1.ServiceExposure, fallback corev1.ServiceType) corev1.ServiceType {
|
||||||
|
switch exposure {
|
||||||
|
case v1alpha1.ExposureClusterIP:
|
||||||
|
return corev1.ServiceTypeClusterIP
|
||||||
|
case v1alpha1.ExposureNodePort:
|
||||||
|
return corev1.ServiceTypeNodePort
|
||||||
|
case v1alpha1.ExposureLoadBalancer:
|
||||||
|
return corev1.ServiceTypeLoadBalancer
|
||||||
|
default:
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HeapMB(limitMB int32, heapPercent int32) string {
|
||||||
|
if heapPercent <= 0 || heapPercent > 100 {
|
||||||
|
heapPercent = DefaultHeapPercent
|
||||||
|
}
|
||||||
|
heap := int64(limitMB) * int64(heapPercent) / 100
|
||||||
|
if heap < 256 {
|
||||||
|
heap = 256
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dM", heap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResourceRequirements(r v1alpha1.Resources) corev1.ResourceRequirements {
|
||||||
|
limitMB := r.MemoryLimitMB
|
||||||
|
if limitMB <= 0 {
|
||||||
|
limitMB = 2048
|
||||||
|
}
|
||||||
|
requestMB := r.MemoryRequestMB
|
||||||
|
if requestMB <= 0 || requestMB > limitMB {
|
||||||
|
requestMB = limitMB
|
||||||
|
}
|
||||||
|
|
||||||
|
requests := corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", requestMB)),
|
||||||
|
}
|
||||||
|
limits := corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", limitMB)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.CPURequest != "" {
|
||||||
|
if q, err := resource.ParseQuantity(r.CPURequest); err == nil {
|
||||||
|
requests[corev1.ResourceCPU] = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.CPULimit != "" {
|
||||||
|
if q, err := resource.ParseQuantity(r.CPULimit); err == nil {
|
||||||
|
limits[corev1.ResourceCPU] = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return corev1.ResourceRequirements{Requests: requests, Limits: limits}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JVMEnv(jvm v1alpha1.JVMOptions, limitMB int32) []corev1.EnvVar {
|
||||||
|
env := []corev1.EnvVar{
|
||||||
|
{Name: "MEMORY", Value: HeapMB(limitMB, jvm.HeapPercent)},
|
||||||
|
}
|
||||||
|
if jvm.Opts != "" {
|
||||||
|
env = append(env, corev1.EnvVar{Name: "JVM_OPTS", Value: jvm.Opts})
|
||||||
|
}
|
||||||
|
if jvm.UseAikarFlags {
|
||||||
|
env = append(env, corev1.EnvVar{Name: "USE_AIKAR_FLAGS", Value: "true"})
|
||||||
|
}
|
||||||
|
if jvm.UseMeowIceFlags {
|
||||||
|
env = append(env, corev1.EnvVar{Name: "USE_MEOWICE_FLAGS", Value: "true"})
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserEnv(base []corev1.EnvVar, extra []v1alpha1.EnvVar) []corev1.EnvVar {
|
||||||
|
for _, e := range extra {
|
||||||
|
base = append(base, corev1.EnvVar{Name: e.Name, Value: e.Value})
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func TCPProbe(initialDelay int32) *corev1.Probe {
|
||||||
|
return &corev1.Probe{
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
TCPSocket: &corev1.TCPSocketAction{
|
||||||
|
Port: intstr.FromInt32(ContainerPort),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitialDelaySeconds: initialDelay,
|
||||||
|
PeriodSeconds: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObjectMeta(name, namespace string, labels map[string]string) metav1.ObjectMeta {
|
||||||
|
return metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolValue(b *bool, fallback bool) bool {
|
||||||
|
if b == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return *b
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeapMB(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
limitMB int32
|
||||||
|
heapPercent int32
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"default percent when unset", 2048, 0, "1638M"},
|
||||||
|
{"explicit percent", 2048, 50, "1024M"},
|
||||||
|
{"out of range falls back", 1024, 150, "819M"},
|
||||||
|
{"floor applies to tiny limits", 128, 80, "256M"},
|
||||||
|
{"full allocation", 1000, 100, "1000M"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := HeapMB(tt.limitMB, tt.heapPercent); got != tt.want {
|
||||||
|
t.Errorf("HeapMB(%d, %d) = %q, want %q", tt.limitMB, tt.heapPercent, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceRequirements(t *testing.T) {
|
||||||
|
t.Run("request defaults to limit", func(t *testing.T) {
|
||||||
|
got := ResourceRequirements(v1alpha1.Resources{MemoryLimitMB: 2048})
|
||||||
|
want := resource.MustParse("2048Mi")
|
||||||
|
if got.Requests.Memory().Cmp(want) != 0 {
|
||||||
|
t.Errorf("request memory = %v, want %v", got.Requests.Memory(), &want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("request above limit is clamped", func(t *testing.T) {
|
||||||
|
got := ResourceRequirements(v1alpha1.Resources{MemoryLimitMB: 1024, MemoryRequestMB: 4096})
|
||||||
|
want := resource.MustParse("1024Mi")
|
||||||
|
if got.Requests.Memory().Cmp(want) != 0 {
|
||||||
|
t.Errorf("request memory = %v, want %v", got.Requests.Memory(), &want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid cpu strings are dropped", func(t *testing.T) {
|
||||||
|
got := ResourceRequirements(v1alpha1.Resources{
|
||||||
|
MemoryLimitMB: 1024,
|
||||||
|
CPURequest: "not-a-quantity",
|
||||||
|
CPULimit: "500m",
|
||||||
|
})
|
||||||
|
if _, ok := got.Requests[corev1.ResourceCPU]; ok {
|
||||||
|
t.Error("expected invalid cpu request to be omitted")
|
||||||
|
}
|
||||||
|
if got.Limits.Cpu().String() != "500m" {
|
||||||
|
t.Errorf("cpu limit = %v, want 500m", got.Limits.Cpu())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceType(t *testing.T) {
|
||||||
|
if got := ServiceType("", corev1.ServiceTypeLoadBalancer); got != corev1.ServiceTypeLoadBalancer {
|
||||||
|
t.Errorf("empty exposure = %v, want fallback LoadBalancer", got)
|
||||||
|
}
|
||||||
|
if got := ServiceType(v1alpha1.ExposureNodePort, corev1.ServiceTypeClusterIP); got != corev1.ServiceTypeNodePort {
|
||||||
|
t.Errorf("NodePort exposure = %v, want NodePort", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserEnvOverridesDefaults(t *testing.T) {
|
||||||
|
base := []corev1.EnvVar{{Name: "TYPE", Value: "VANILLA"}}
|
||||||
|
got := UserEnv(base, []v1alpha1.EnvVar{{Name: "TYPE", Value: "PAPER"}})
|
||||||
|
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("len = %d, want 2", len(got))
|
||||||
|
}
|
||||||
|
if got[len(got)-1].Value != "PAPER" {
|
||||||
|
t.Errorf("last TYPE = %q, want PAPER", got[len(got)-1].Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MinecraftConfigMap(mc *v1alpha1.MinecraftServer) *corev1.ConfigMap {
|
||||||
|
name := ServerName(mc.Name)
|
||||||
|
return &corev1.ConfigMap{
|
||||||
|
ObjectMeta: ObjectMeta(ConfigMapName(name), mc.Namespace, ServerLabels(mc)),
|
||||||
|
Data: map[string]string{
|
||||||
|
"server-type": string(mc.Spec.Type),
|
||||||
|
"minecraft-version": mc.Spec.MinecraftVersion,
|
||||||
|
"jar-type": string(mc.Spec.JarType),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinecraftService(mc *v1alpha1.MinecraftServer) *corev1.Service {
|
||||||
|
name := ServerName(mc.Name)
|
||||||
|
port := corev1.ServicePort{
|
||||||
|
Name: "minecraft",
|
||||||
|
Port: mc.Spec.ListenPort,
|
||||||
|
TargetPort: intstr.FromInt32(ContainerPort),
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
svcType := ServiceType(mc.Spec.ServiceType, corev1.ServiceTypeClusterIP)
|
||||||
|
if svcType == corev1.ServiceTypeNodePort && mc.Spec.NodePort != 0 {
|
||||||
|
port.NodePort = mc.Spec.NodePort
|
||||||
|
}
|
||||||
|
|
||||||
|
return &corev1.Service{
|
||||||
|
ObjectMeta: ObjectMeta(name, mc.Namespace, ServerLabels(mc)),
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: SelectorLabels(name),
|
||||||
|
Ports: []corev1.ServicePort{port},
|
||||||
|
Type: svcType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func minecraftEnv(mc *v1alpha1.MinecraftServer) []corev1.EnvVar {
|
||||||
|
p := mc.Spec.Properties
|
||||||
|
|
||||||
|
env := []corev1.EnvVar{
|
||||||
|
{Name: "EULA", Value: "TRUE"},
|
||||||
|
{Name: "TYPE", Value: string(mc.Spec.JarType)},
|
||||||
|
{Name: "VERSION", Value: mc.Spec.MinecraftVersion},
|
||||||
|
{Name: "OVERRIDE_SERVER_PROPERTIES", Value: "true"},
|
||||||
|
{Name: "ENABLE_RCON", Value: "false"},
|
||||||
|
{Name: "DIFFICULTY", Value: string(p.Difficulty)},
|
||||||
|
{Name: "MODE", Value: string(p.GameMode)},
|
||||||
|
{Name: "MAX_PLAYERS", Value: strconv.Itoa(int(p.MaxPlayers))},
|
||||||
|
{Name: "PVP", Value: strconv.FormatBool(BoolValue(p.PVP, true))},
|
||||||
|
{Name: "ONLINE_MODE", Value: strconv.FormatBool(BoolValue(p.OnlineMode, true))},
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MOTD != "" {
|
||||||
|
env = append(env, corev1.EnvVar{Name: "MOTD", Value: p.MOTD})
|
||||||
|
}
|
||||||
|
if p.LevelSeed != "" {
|
||||||
|
env = append(env, corev1.EnvVar{Name: "SEED", Value: p.LevelSeed})
|
||||||
|
}
|
||||||
|
if p.LevelType != "" {
|
||||||
|
env = append(env, corev1.EnvVar{Name: "LEVEL_TYPE", Value: p.LevelType})
|
||||||
|
}
|
||||||
|
|
||||||
|
env = append(env, JVMEnv(mc.Spec.JVM, mc.Spec.Resources.MemoryLimitMB)...)
|
||||||
|
|
||||||
|
if mc.Spec.APIKeySecretRef != "" {
|
||||||
|
env = append(env, corev1.EnvVar{
|
||||||
|
Name: "MINIKURA_API_KEY",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{Name: mc.Spec.APIKeySecretRef},
|
||||||
|
Key: "api-key",
|
||||||
|
Optional: ptr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserEnv(env, mc.Spec.Env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func minecraftPodSpec(mc *v1alpha1.MinecraftServer, stateful bool) corev1.PodSpec {
|
||||||
|
name := ServerName(mc.Name)
|
||||||
|
|
||||||
|
mounts := []corev1.VolumeMount{{Name: "config", MountPath: "/config"}}
|
||||||
|
volumes := []corev1.Volume{{
|
||||||
|
Name: "config",
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{Name: ConfigMapName(name)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
initialDelay := int32(30)
|
||||||
|
if stateful {
|
||||||
|
mounts = append(mounts, corev1.VolumeMount{Name: "data", MountPath: "/data"})
|
||||||
|
initialDelay = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
return corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{{
|
||||||
|
Name: "minecraft",
|
||||||
|
Image: MinecraftImage,
|
||||||
|
Ports: []corev1.ContainerPort{{
|
||||||
|
Name: "minecraft",
|
||||||
|
ContainerPort: ContainerPort,
|
||||||
|
}},
|
||||||
|
Env: minecraftEnv(mc),
|
||||||
|
VolumeMounts: mounts,
|
||||||
|
ReadinessProbe: TCPProbe(initialDelay),
|
||||||
|
Resources: ResourceRequirements(mc.Spec.Resources),
|
||||||
|
}},
|
||||||
|
Volumes: volumes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinecraftDeployment(mc *v1alpha1.MinecraftServer) *appsv1.Deployment {
|
||||||
|
name := ServerName(mc.Name)
|
||||||
|
labels := ServerLabels(mc)
|
||||||
|
|
||||||
|
return &appsv1.Deployment{
|
||||||
|
ObjectMeta: ObjectMeta(name, mc.Namespace, labels),
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Replicas: ptr(int32(1)),
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: SelectorLabels(name)},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Labels: labels},
|
||||||
|
Spec: minecraftPodSpec(mc, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinecraftStatefulSet(mc *v1alpha1.MinecraftServer) (*appsv1.StatefulSet, error) {
|
||||||
|
name := ServerName(mc.Name)
|
||||||
|
labels := ServerLabels(mc)
|
||||||
|
|
||||||
|
size := mc.Spec.StorageSize
|
||||||
|
if size == "" {
|
||||||
|
size = "1Gi"
|
||||||
|
}
|
||||||
|
qty, err := resource.ParseQuantity(size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid storageSize %q: %w", size, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &appsv1.StatefulSet{
|
||||||
|
ObjectMeta: ObjectMeta(name, mc.Namespace, labels),
|
||||||
|
Spec: appsv1.StatefulSetSpec{
|
||||||
|
ServiceName: name,
|
||||||
|
Replicas: ptr(int32(1)),
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: SelectorLabels(name)},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Labels: labels},
|
||||||
|
Spec: minecraftPodSpec(mc, true),
|
||||||
|
},
|
||||||
|
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "data"},
|
||||||
|
Spec: corev1.PersistentVolumeClaimSpec{
|
||||||
|
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
||||||
|
Resources: corev1.VolumeResourceRequirements{
|
||||||
|
Requests: corev1.ResourceList{corev1.ResourceStorage: qty},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](v T) *T { return &v }
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testServer() *v1alpha1.MinecraftServer {
|
||||||
|
return &v1alpha1.MinecraftServer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "smp", Namespace: "minikura"},
|
||||||
|
Spec: v1alpha1.MinecraftServerSpec{
|
||||||
|
Type: v1alpha1.ServerStateful,
|
||||||
|
ListenPort: 25565,
|
||||||
|
ServiceType: v1alpha1.ExposureClusterIP,
|
||||||
|
JarType: "PAPER",
|
||||||
|
MinecraftVersion: "1.20.4",
|
||||||
|
StorageSize: "10Gi",
|
||||||
|
Resources: v1alpha1.Resources{MemoryLimitMB: 4096},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envValue(env []corev1.EnvVar, name string) (string, bool) {
|
||||||
|
for i := len(env) - 1; i >= 0; i-- {
|
||||||
|
if env[i].Name == name {
|
||||||
|
return env[i].Value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinecraftEnvCarriesSpecConfig(t *testing.T) {
|
||||||
|
mc := testServer()
|
||||||
|
mc.Spec.Properties = v1alpha1.MinecraftProperties{
|
||||||
|
Difficulty: "HARD",
|
||||||
|
GameMode: "SURVIVAL",
|
||||||
|
MaxPlayers: 50,
|
||||||
|
MOTD: "welcome",
|
||||||
|
}
|
||||||
|
|
||||||
|
env := minecraftEnv(mc)
|
||||||
|
|
||||||
|
for _, want := range []struct{ key, value string }{
|
||||||
|
{"TYPE", "PAPER"},
|
||||||
|
{"VERSION", "1.20.4"},
|
||||||
|
{"DIFFICULTY", "HARD"},
|
||||||
|
{"MAX_PLAYERS", "50"},
|
||||||
|
{"MOTD", "welcome"},
|
||||||
|
} {
|
||||||
|
got, ok := envValue(env, want.key)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s missing from env", want.key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got != want.value {
|
||||||
|
t.Errorf("%s = %q, want %q", want.key, got, want.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalPropertiesOmitted(t *testing.T) {
|
||||||
|
env := minecraftEnv(testServer())
|
||||||
|
for _, key := range []string{"MOTD", "SEED", "LEVEL_TYPE"} {
|
||||||
|
if _, ok := envValue(env, key); ok {
|
||||||
|
t.Errorf("%s should be omitted when unset", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceNodePortPinned(t *testing.T) {
|
||||||
|
mc := testServer()
|
||||||
|
mc.Spec.ServiceType = v1alpha1.ExposureNodePort
|
||||||
|
mc.Spec.NodePort = 30123
|
||||||
|
|
||||||
|
svc := MinecraftService(mc)
|
||||||
|
if svc.Spec.Type != corev1.ServiceTypeNodePort {
|
||||||
|
t.Fatalf("type = %v, want NodePort", svc.Spec.Type)
|
||||||
|
}
|
||||||
|
if svc.Spec.Ports[0].NodePort != 30123 {
|
||||||
|
t.Errorf("nodePort = %d, want 30123", svc.Spec.Ports[0].NodePort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceNodePortUnsetLeavesAllocation(t *testing.T) {
|
||||||
|
mc := testServer()
|
||||||
|
mc.Spec.ServiceType = v1alpha1.ExposureNodePort
|
||||||
|
|
||||||
|
svc := MinecraftService(mc)
|
||||||
|
if svc.Spec.Ports[0].NodePort != 0 {
|
||||||
|
t.Errorf("nodePort = %d, want 0 so the API server allocates one", svc.Spec.Ports[0].NodePort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectorExcludesMutableLabels(t *testing.T) {
|
||||||
|
mc := testServer()
|
||||||
|
sts, err := MinecraftStatefulSet(mc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selector := sts.Spec.Selector.MatchLabels
|
||||||
|
if _, ok := selector[v1alpha1.LabelServerType]; ok {
|
||||||
|
t.Error("selector must not include the mutable server-type label")
|
||||||
|
}
|
||||||
|
if selector["app"] != ServerName(mc.Name) {
|
||||||
|
t.Errorf("selector app = %q, want %q", selector["app"], ServerName(mc.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range selector {
|
||||||
|
if sts.Spec.Template.Labels[k] != v {
|
||||||
|
t.Errorf("pod template missing selector label %s=%s", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatefulSetStorage(t *testing.T) {
|
||||||
|
sts, err := MinecraftStatefulSet(testServer())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := sts.Spec.VolumeClaimTemplates
|
||||||
|
if len(claims) != 1 {
|
||||||
|
t.Fatalf("volumeClaimTemplates = %d, want 1", len(claims))
|
||||||
|
}
|
||||||
|
if got := claims[0].Spec.Resources.Requests.Storage().String(); got != "10Gi" {
|
||||||
|
t.Errorf("storage = %s, want 10Gi", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatefulSetRejectsBadStorageSize(t *testing.T) {
|
||||||
|
mc := testServer()
|
||||||
|
mc.Spec.StorageSize = "ten-gigs"
|
||||||
|
|
||||||
|
if _, err := MinecraftStatefulSet(mc); err == nil {
|
||||||
|
t.Error("expected an error for an unparseable storageSize")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatelessHasNoDataVolume(t *testing.T) {
|
||||||
|
mc := testServer()
|
||||||
|
mc.Spec.Type = v1alpha1.ServerStateless
|
||||||
|
|
||||||
|
dep := MinecraftDeployment(mc)
|
||||||
|
for _, m := range dep.Spec.Template.Spec.Containers[0].VolumeMounts {
|
||||||
|
if m.Name == "data" {
|
||||||
|
t.Error("stateless server should not mount a data volume")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServerName(name string) string { return fmt.Sprintf("minecraft-%s", name) }
|
||||||
|
func ProxyName(kind v1alpha1.ProxyKind, name string) string {
|
||||||
|
return fmt.Sprintf("%s-%s", strings.ToLower(string(kind)), name)
|
||||||
|
}
|
||||||
|
func ConfigMapName(base string) string { return base + "-config" }
|
||||||
|
|
||||||
|
func ServerLabels(mc *v1alpha1.MinecraftServer) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"app": ServerName(mc.Name),
|
||||||
|
v1alpha1.LabelServerType: strings.ToLower(string(mc.Spec.Type)),
|
||||||
|
v1alpha1.LabelServerID: mc.Name,
|
||||||
|
v1alpha1.LabelManagedBy: v1alpha1.ManagerName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyLabels(rp *v1alpha1.ReverseProxyServer) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"app": ProxyName(rp.Spec.Type, rp.Name),
|
||||||
|
v1alpha1.LabelServerType: strings.ToLower(string(rp.Spec.Type)),
|
||||||
|
v1alpha1.LabelProxyID: rp.Name,
|
||||||
|
v1alpha1.LabelManagedBy: v1alpha1.ManagerName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectorLabels(app string) map[string]string {
|
||||||
|
return map[string]string{"app": app}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProxyConfigMap(rp *v1alpha1.ReverseProxyServer) *corev1.ConfigMap {
|
||||||
|
name := ProxyName(rp.Spec.Type, rp.Name)
|
||||||
|
return &corev1.ConfigMap{
|
||||||
|
ObjectMeta: ObjectMeta(ConfigMapName(name), rp.Namespace, ProxyLabels(rp)),
|
||||||
|
Data: map[string]string{
|
||||||
|
"proxy-type": string(rp.Spec.Type),
|
||||||
|
"external-address": rp.Spec.ExternalAddress,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyService(rp *v1alpha1.ReverseProxyServer) *corev1.Service {
|
||||||
|
name := ProxyName(rp.Spec.Type, rp.Name)
|
||||||
|
port := corev1.ServicePort{
|
||||||
|
Name: "minecraft",
|
||||||
|
Port: rp.Spec.ExternalPort,
|
||||||
|
TargetPort: intstr.FromInt32(rp.Spec.ListenPort),
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
svcType := ServiceType(rp.Spec.ServiceType, corev1.ServiceTypeLoadBalancer)
|
||||||
|
if svcType == corev1.ServiceTypeNodePort && rp.Spec.NodePort != 0 {
|
||||||
|
port.NodePort = rp.Spec.NodePort
|
||||||
|
}
|
||||||
|
|
||||||
|
return &corev1.Service{
|
||||||
|
ObjectMeta: ObjectMeta(name, rp.Namespace, ProxyLabels(rp)),
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: SelectorLabels(name),
|
||||||
|
Ports: []corev1.ServicePort{port},
|
||||||
|
Type: svcType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyEnv(rp *v1alpha1.ReverseProxyServer) []corev1.EnvVar {
|
||||||
|
env := []corev1.EnvVar{
|
||||||
|
{Name: "EULA", Value: "TRUE"},
|
||||||
|
{Name: "TYPE", Value: string(rp.Spec.Type)},
|
||||||
|
{Name: "MINIKURA_EXTERNAL_ADDRESS", Value: rp.Spec.ExternalAddress},
|
||||||
|
}
|
||||||
|
|
||||||
|
env = append(env, JVMEnv(rp.Spec.JVM, rp.Spec.Resources.MemoryLimitMB)...)
|
||||||
|
|
||||||
|
if rp.Spec.APIKeySecretRef != "" {
|
||||||
|
env = append(env, corev1.EnvVar{
|
||||||
|
Name: "MINIKURA_API_KEY",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{Name: rp.Spec.APIKeySecretRef},
|
||||||
|
Key: "api-key",
|
||||||
|
Optional: ptr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserEnv(env, rp.Spec.Env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyDeployment(rp *v1alpha1.ReverseProxyServer) *appsv1.Deployment {
|
||||||
|
name := ProxyName(rp.Spec.Type, rp.Name)
|
||||||
|
labels := ProxyLabels(rp)
|
||||||
|
|
||||||
|
return &appsv1.Deployment{
|
||||||
|
ObjectMeta: ObjectMeta(name, rp.Namespace, labels),
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Replicas: ptr(int32(1)),
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: SelectorLabels(name)},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Labels: labels},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{{
|
||||||
|
Name: "proxy",
|
||||||
|
Image: MinecraftImage,
|
||||||
|
Ports: []corev1.ContainerPort{{
|
||||||
|
Name: "minecraft",
|
||||||
|
ContainerPort: rp.Spec.ListenPort,
|
||||||
|
}},
|
||||||
|
Env: proxyEnv(rp),
|
||||||
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
|
{Name: "config", MountPath: "/config"},
|
||||||
|
},
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
|
TCPSocket: &corev1.TCPSocketAction{
|
||||||
|
Port: intstr.FromInt32(rp.Spec.ListenPort),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitialDelaySeconds: 30,
|
||||||
|
PeriodSeconds: 10,
|
||||||
|
},
|
||||||
|
Resources: ResourceRequirements(rp.Spec.Resources),
|
||||||
|
}},
|
||||||
|
Volumes: []corev1.Volume{{
|
||||||
|
Name: "config",
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: ConfigMapName(name),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user