♻️ refactor: migrate to Go Kubernetes operator

This commit is contained in:
2026-08-13 02:19:27 +07:00
parent 826464a9e5
commit 91c07eb76a
50 changed files with 532 additions and 2930 deletions
+9 -3
View File
@@ -1,5 +1,6 @@
IMG ?= minikura-operator:latest
NAMESPACE ?= minikura
CONTROLLER_GEN_VERSION ?= v0.20.0
CONTROLLER_GEN ?= $(shell go env GOPATH)/bin/controller-gen
.PHONY: all
@@ -7,7 +8,7 @@ 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
@test -x $(CONTROLLER_GEN) || go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION)
.PHONY: generate
generate: controller-gen
@@ -52,6 +53,11 @@ uninstall-crds:
.PHONY: deploy
deploy: manifests
kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f config/crd
kubectl apply -n $(NAMESPACE) -f config/rbac
kubectl apply -n $(NAMESPACE) -f config/manager
kubectl apply -f config/rbac/role.yaml
kubectl apply -n $(NAMESPACE) -f config/rbac/service_account.yaml
kubectl apply -n $(NAMESPACE) -f config/rbac/backend.yaml
kubectl patch clusterrolebinding minikura-operator-rolebinding --type=json -p='[{"op":"replace","path":"/subjects/0/namespace","value":"$(NAMESPACE)"}]'
kubectl patch clusterrolebinding minikura-backend-operator-resources --type=json -p='[{"op":"replace","path":"/subjects/0/namespace","value":"$(NAMESPACE)"}]'
sed 's|minikura-operator:latest|$(IMG)|' config/manager/deployment.yaml | kubectl apply -n $(NAMESPACE) -f -
@@ -141,7 +141,7 @@ type MinecraftServerStatus struct {
// +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="Ready",type=integer,JSONPath=`.status.readyReplicas`
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.status.endpoint`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
type MinecraftServer struct {
@@ -89,7 +89,7 @@ type ReverseProxyServerStatus struct {
// +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="Ready",type=integer,JSONPath=`.status.readyReplicas`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
type ReverseProxyServer struct {
metav1.TypeMeta `json:",inline"`
@@ -28,7 +28,7 @@ spec:
type: string
- jsonPath: .status.readyReplicas
name: Ready
type: string
type: integer
- jsonPath: .status.endpoint
name: Endpoint
type: string
@@ -28,7 +28,7 @@ spec:
type: string
- jsonPath: .status.readyReplicas
name: Ready
type: string
type: integer
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
-2
View File
@@ -29,8 +29,6 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_ENV
value: production
ports:
- name: metrics
containerPort: 8080
+29
View File
@@ -0,0 +1,29 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: minikura-backend
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: minikura-backend-operator-resources
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: ["minikura.kirameki.cafe"]
resources: ["minecraftservers", "reverseproxyservers"]
verbs: ["get", "list", "create", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: minikura-backend-operator-resources
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: minikura-backend-operator-resources
subjects:
- kind: ServiceAccount
name: minikura-backend
namespace: minikura
@@ -50,10 +50,38 @@ func (r *ReverseProxyServerReconciler) Reconcile(ctx context.Context, req ctrl.R
if err := apply(ctx, r.Client, &rp, resources.ProxyDeployment(&rp), r.Scheme); err != nil {
return r.fail(ctx, &rp, "DeploymentFailed", err)
}
if err := r.pruneStaleResources(ctx, &rp); err != nil {
return r.fail(ctx, &rp, "PruneFailed", err)
}
return ctrl.Result{}, r.updateStatus(ctx, &rp)
}
func (r *ReverseProxyServerReconciler) pruneStaleResources(ctx context.Context, rp *v1alpha1.ReverseProxyServer) error {
current := resources.ProxyName(rp.Spec.Type, rp.Name)
for _, kind := range []v1alpha1.ProxyKind{v1alpha1.ProxyVelocity, v1alpha1.ProxyBungeeCord} {
name := resources.ProxyName(kind, rp.Name)
if name == current {
continue
}
for _, obj := range []client.Object{&appsv1.Deployment{}, &corev1.Service{}, &corev1.ConfigMap{}} {
key := client.ObjectKey{Name: name, Namespace: rp.Namespace}
if err := r.Get(ctx, key, obj); err != nil {
if apierrors.IsNotFound(err) {
continue
}
return err
}
if metav1.IsControlledBy(obj, rp) {
if err := r.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) {
return err
}
}
}
}
return nil
}
func (r *ReverseProxyServerReconciler) backends(ctx context.Context, rp *v1alpha1.ReverseProxyServer) ([]string, error) {
selector := labels.Everything()
if rp.Spec.BackendSelector != nil {
+1
View File
@@ -13,6 +13,7 @@ import (
const (
MinecraftImage = "itzg/minecraft-server"
ProxyImage = "itzg/mc-proxy:latest"
ContainerPort = 25565
DefaultHeapPercent = 80
)
@@ -46,8 +46,8 @@ func ProxyService(rp *v1alpha1.ReverseProxyServer) *corev1.Service {
func proxyEnv(rp *v1alpha1.ReverseProxyServer) []corev1.EnvVar {
env := []corev1.EnvVar{
{Name: "EULA", Value: "TRUE"},
{Name: "TYPE", Value: string(rp.Spec.Type)},
{Name: "NETWORKADDRESS_CACHE_TTL", Value: "30"},
{Name: "MINIKURA_EXTERNAL_ADDRESS", Value: rp.Spec.ExternalAddress},
}
@@ -83,7 +83,7 @@ func ProxyDeployment(rp *v1alpha1.ReverseProxyServer) *appsv1.Deployment {
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "proxy",
Image: MinecraftImage,
Image: ProxyImage,
Ports: []corev1.ContainerPort{{
Name: "minecraft",
ContainerPort: rp.Spec.ListenPort,
@@ -0,0 +1,43 @@
package resources
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
)
func testProxy() *v1alpha1.ReverseProxyServer {
return &v1alpha1.ReverseProxyServer{
ObjectMeta: metav1.ObjectMeta{Name: "edge", Namespace: "minikura"},
Spec: v1alpha1.ReverseProxyServerSpec{
Type: v1alpha1.ProxyVelocity,
ExternalAddress: "play.example.com",
ExternalPort: 25565,
ListenPort: 25577,
Resources: v1alpha1.Resources{MemoryLimitMB: 512},
},
}
}
func TestProxyDeploymentUsesProxyImage(t *testing.T) {
container := ProxyDeployment(testProxy()).Spec.Template.Spec.Containers[0]
if container.Image != ProxyImage {
t.Errorf("image = %q, want %q", container.Image, ProxyImage)
}
}
func TestProxyEnvCarriesRuntimeConfig(t *testing.T) {
env := proxyEnv(testProxy())
for _, want := range []struct{ key, value string }{
{"TYPE", "VELOCITY"},
{"NETWORKADDRESS_CACHE_TTL", "30"},
{"MINIKURA_EXTERNAL_ADDRESS", "play.example.com"},
} {
got, ok := envValue(env, want.key)
if !ok || got != want.value {
t.Errorf("%s = %q, %v; want %q", want.key, got, ok, want.value)
}
}
}