diff --git a/api/v1alpha1/servermanager_types.go b/api/v1alpha1/servermanager_types.go index 474d647..d0260da 100644 --- a/api/v1alpha1/servermanager_types.go +++ b/api/v1alpha1/servermanager_types.go @@ -50,14 +50,13 @@ type ServerSpec struct { } type BrowserSpec struct { - Image string `json:"image,omitempty"` - On bool `json:"on,omitempty"` - Port Port `json:"port,omitempty"` + On bool `json:"on,omitempty"` } // ServerManagerSpec defines the desired state of ServerManager type ServerManagerSpec struct { - Id string `json:"id,omitempty"` + Id string `json:"id,omitempty"` + Storage string `json:"storage,omitempty"` // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file Server ServerSpec `json:"server,omitempty"` diff --git a/cmd/main.go b/cmd/main.go index a84ccd3..38594c9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,6 +23,7 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "gopkg.in/yaml.v3" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/apimachinery/pkg/runtime" @@ -37,6 +38,7 @@ import ( servermanagerv1alpha1 "git.acooldomain.co/server-manager/kubernetes-operator/api/v1alpha1" "git.acooldomain.co/server-manager/kubernetes-operator/internal/controller" + traefikv3 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -49,6 +51,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(servermanagerv1alpha1.AddToScheme(scheme)) + utilruntime.Must(traefikv3.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -143,12 +146,20 @@ func main() { setupLog.Error(err, "unable to start manager") os.Exit(1) } - config := controller.ServerManagerReconcilerConfig{} + config := &controller.ServerManagerReconcilerConfig{} + configData, err := os.ReadFile("config.yaml") + if err != nil { + setupLog.Error(err, "unable to read config file") + } + err = yaml.Unmarshal(configData, config) + if err != nil { + setupLog.Error(err, "failed to marshal data") + } if err = (&controller.ServerManagerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Config: config, + Config: *config, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ServerManager") os.Exit(1) diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..25807a8 --- /dev/null +++ b/config.yaml @@ -0,0 +1,2 @@ +domain_label: "ddns.acooldomain.co/hostname" +default_domain: "acooldomain.co" diff --git a/config/crd/bases/server-manager.acooldomain.co_servermanagers.yaml b/config/crd/bases/server-manager.acooldomain.co_servermanagers.yaml index 4253e88..26ecfc2 100644 --- a/config/crd/bases/server-manager.acooldomain.co_servermanagers.yaml +++ b/config/crd/bases/server-manager.acooldomain.co_servermanagers.yaml @@ -90,6 +90,8 @@ spec: working_dir: type: string type: object + storage: + type: string type: object status: description: ServerManagerStatus defines the observed state of ServerManager diff --git a/config/samples/server-manager_v1alpha1_servermanager.yaml b/config/samples/server-manager_v1alpha1_servermanager.yaml index 2b2a5a3..4963995 100644 --- a/config/samples/server-manager_v1alpha1_servermanager.yaml +++ b/config/samples/server-manager_v1alpha1_servermanager.yaml @@ -6,4 +6,12 @@ metadata: app.kubernetes.io/managed-by: kustomize name: servermanager-sample spec: - # TODO(user): Add fields here + id: test-serverr + storage: 10Gi + server: + "on": true + image: git.acooldomain.co/server-manager/minecraft:paper-1.21.4 + working_dir: /opt/server + ports: + - protocol: TCP + port: 25565 diff --git a/internal/controller/servermanager_controller.go b/internal/controller/servermanager_controller.go index dfc5462..4b84435 100644 --- a/internal/controller/servermanager_controller.go +++ b/internal/controller/servermanager_controller.go @@ -19,10 +19,13 @@ package controller import ( "context" "fmt" + "strings" + "time" traefikv3 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" @@ -34,9 +37,19 @@ import ( servermanagerv1alpha1 "git.acooldomain.co/server-manager/kubernetes-operator/api/v1alpha1" ) +type MiddlewareRef struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` +} + +type BrowserConfig struct { + Middleware MiddlewareRef `yaml:"middleware"` +} + type ServerManagerReconcilerConfig struct { - DomainLabel string `yaml:"domain_label"` - DefaultDomain string `yaml:"default_domain"` + Browser BrowserConfig `yaml:"browser_middleware"` + DomainLabel string `yaml:"domain_label"` + DefaultDomain string `yaml:"default_domain"` } // ServerManagerReconciler reconciles a ServerManager object @@ -84,63 +97,142 @@ func (r *ServerManagerReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } } + logging.Info("verified pvc") serverPod := r.ServerPod(s, pvc) - if serverPod != nil { - found := &corev1.Pod{} - err = r.Get(ctx, client.ObjectKey{Namespace: pvc.Namespace, Name: pvc.Name}, found) - if err != nil { - if errors.IsNotFound(err) { + found := &corev1.Pod{} + err = r.Get(ctx, client.ObjectKey{Namespace: pvc.Namespace, Name: pvc.Name}, found) + if err == nil && !s.Spec.Server.On { + err = r.Delete(ctx, serverPod) + return ctrl.Result{Requeue: true}, err + } + + if err != nil { + if errors.IsNotFound(err) { + if s.Spec.Server.On { err = r.Create(ctx, serverPod) return ctrl.Result{Requeue: true}, err - } else { - return ctrl.Result{}, err } + } else { + return ctrl.Result{}, err } + } - domain := r.Config.DefaultDomain - if r.Config.DomainLabel != "" { - node := &corev1.Node{} - err = r.Get(ctx, client.ObjectKeyFromObject(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: found.Spec.NodeName}}), node) - if err != nil { - logging.Error(err, fmt.Sprintf("Failed to find node %s", found.Spec.NodeName)) - return ctrl.Result{}, err - } + logging.Info("verified pod") - labelDomain, ok := node.GetLabels()[r.Config.DomainLabel] - if ok { - domain = labelDomain - } - } + if found.Spec.NodeName == "" { + logging.Info("waiting for pod to start 2") + return ctrl.Result{RequeueAfter: time.Second * 10}, nil + } - if domain != s.Status.Server.Domain { - s.Status.Server.Domain = domain - err = r.Update(ctx, s) + domain := r.Config.DefaultDomain + if r.Config.DomainLabel != "" { + node := &corev1.Node{} + err = r.Get(ctx, client.ObjectKeyFromObject(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: found.Spec.NodeName}}), node) + if err != nil { + logging.Error(err, fmt.Sprintf("Failed to find node %s", found.Spec.NodeName)) return ctrl.Result{}, err } + labelDomain, ok := node.GetLabels()[r.Config.DomainLabel] + if ok { + domain = labelDomain + } + } + + logging.Info("got domain", "domain", domain) + + if domain != s.Status.Server.Domain { + s.Status.Server.Domain = domain + logging.Info("updating ServerManager object", "NewDomain", domain, "OldDomain", s.Status.Server.Domain) + err = r.Status().Update(ctx, s) + logging.Info(fmt.Sprintf("%#v", err)) + return ctrl.Result{}, err } service := r.ServerService(s) - found_service := &corev1.Service{} - err = r.Get(ctx, client.ObjectKeyFromObject(service), found_service) + foundService := &corev1.Service{} + err = r.Get(ctx, client.ObjectKeyFromObject(service), foundService) + if err == nil && !s.Spec.Server.On { + err = r.Delete(ctx, service) + return ctrl.Result{Requeue: true}, err + } if err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, err } - err = r.Create(ctx, service) - if err != nil { - return ctrl.Result{}, err + if s.Spec.Server.On { + err = r.Create(ctx, service) + return ctrl.Result{Requeue: true}, err + } + } + logging.Info(fmt.Sprintf("verified service %#v", foundService)) + + return ctrl.Result{}, nil +} + +func (r *ServerManagerReconciler) BrowserPod(s *servermanagerv1alpha1.ServerManager, pvc *corev1.PersistentVolumeClaim) *corev1.Pod { + ports := make([]corev1.ContainerPort, len(s.Spec.Server.Ports)) + + for i, port := range s.Spec.Server.Ports { + ports[i] = corev1.ContainerPort{ + ContainerPort: port.Port, + Protocol: port.Protocol, } } - return ctrl.Result{}, nil + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-browser", s.Name), + Namespace: s.Namespace, + Labels: map[string]string{"server": s.Name}, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "volume", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvc.Name, + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "proxy-setter", + Image: "filebrowser/filebrowser", + ImagePullPolicy: corev1.PullAlways, + WorkingDir: s.Spec.Server.WorkingDir, + Ports: ports, + Args: []string{}, + }, + }, + Containers: []corev1.Container{ + { + Name: "browser", + Image: "filebrowser/filebrowser", + ImagePullPolicy: corev1.PullAlways, + Ports: ports, + Args: []string{}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "volume", + MountPath: s.Spec.Server.WorkingDir, + }}, + Stdin: true, + TTY: true, + }, + }, + }, + } + controllerutil.SetControllerReference(s, pod, r.Scheme) + return pod } func (r *ServerManagerReconciler) ServerService(s *servermanagerv1alpha1.ServerManager) *corev1.Service { ports := make([]corev1.ServicePort, len(s.Spec.Server.Ports)) for i, port := range s.Spec.Server.Ports { - ports[i] = corev1.ServicePort{NodePort: 0, Port: port.Port, TargetPort: intstr.FromInt32(port.Port), Name: fmt.Sprintf("%s-%d", port.Protocol, port.Port)} + ports[i] = corev1.ServicePort{NodePort: 0, Port: port.Port, TargetPort: intstr.FromInt32(port.Port), Name: fmt.Sprintf("%s-%d", strings.ToLower(string(port.Protocol)), port.Port)} } service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -148,9 +240,10 @@ func (r *ServerManagerReconciler) ServerService(s *servermanagerv1alpha1.ServerM Namespace: s.Namespace, }, Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, Ports: ports, Selector: map[string]string{ - "server": s.Spec.Id, + "server": s.Name, }, }, } @@ -167,6 +260,9 @@ func (r *ServerManagerReconciler) ServerPvc(s *servermanagerv1alpha1.ServerManag Spec: corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(s.Spec.Storage)}, + }, }, } @@ -176,9 +272,6 @@ func (r *ServerManagerReconciler) ServerPvc(s *servermanagerv1alpha1.ServerManag } func (r *ServerManagerReconciler) ServerPod(s *servermanagerv1alpha1.ServerManager, pvc *corev1.PersistentVolumeClaim) *corev1.Pod { - if !s.Spec.Server.On { - return nil - } ports := make([]corev1.ContainerPort, len(s.Spec.Server.Ports)) for i, port := range s.Spec.Server.Ports { @@ -192,6 +285,7 @@ func (r *ServerManagerReconciler) ServerPod(s *servermanagerv1alpha1.ServerManag ObjectMeta: metav1.ObjectMeta{ Name: s.Name, Namespace: s.Namespace, + Labels: map[string]string{"server": s.Name}, }, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ @@ -206,12 +300,13 @@ func (r *ServerManagerReconciler) ServerPod(s *servermanagerv1alpha1.ServerManag }, Containers: []corev1.Container{ { - Name: "server", - Image: s.Spec.Server.Image, - Command: s.Spec.Server.Command, - Args: s.Spec.Server.Args, - WorkingDir: s.Spec.Server.WorkingDir, - Ports: ports, + Name: "server", + Image: s.Spec.Server.Image, + ImagePullPolicy: corev1.PullAlways, + Command: s.Spec.Server.Command, + Args: s.Spec.Server.Args, + WorkingDir: s.Spec.Server.WorkingDir, + Ports: ports, VolumeMounts: []corev1.VolumeMount{{ Name: "volume", MountPath: s.Spec.Server.WorkingDir,