package kubernetes import ( "context" "io" "os" "path/filepath" "strings" "time" "git.acooldomain.co/server-manager/backend/instancemanager" "git.acooldomain.co/server-manager/backend/models" servermanagerv1 "git.acooldomain.co/server-manager/kubernetes-operator/api/v1alpha1" "github.com/buildkite/shellwords" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" names "k8s.io/apiserver/pkg/storage/names" kubernetesclient "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/remotecommand" "sigs.k8s.io/controller-runtime/pkg/client" ) type InstanceManager struct { instancemanager.InstanceManager Config models.KubernetesInstanceManagerConfig client client.Client restCfg *rest.Config coreV1Cli *kubernetesclient.Clientset } func convertServerManagerImage(image *servermanagerv1.Image) *instancemanager.Image { ports := make([]instancemanager.Port, len(image.Spec.Ports)) for i, port := range image.Spec.Ports { ports[i] = instancemanager.Port{ Number: uint16(port.Port), Protocol: models.PortProtocol(port.Protocol), } } return &instancemanager.Image{ Registry: image.Spec.Name, Tag: image.Spec.Tag, Id: image.Name, Ports: ports, WorkingDir: image.Spec.WorkingDir, Command: strings.Join(image.Spec.Args, " "), } } // General // Read Only func (i *InstanceManager) GetImage(ctx context.Context, imageId string) (*instancemanager.Image, error) { if imageId == "" { return &instancemanager.Image{}, nil } image := &servermanagerv1.Image{} err := i.client.Get(ctx, client.ObjectKey{Name: imageId, Namespace: i.Config.Namespace}, image) if err != nil { return nil, err } return convertServerManagerImage(image), nil } func (i *InstanceManager) ListImages(ctx context.Context) ([]instancemanager.Image, error) { images := &servermanagerv1.ImageList{} err := i.client.List(ctx, images, &client.ListOptions{Namespace: i.Config.Namespace}) if err != nil { return nil, err } imImages := make([]instancemanager.Image, len(images.Items)) for i, image := range images.Items { imImages[i] = *convertServerManagerImage(&image) } return imImages, nil } func (i *InstanceManager) GetServer(ctx context.Context, serverId string) (*instancemanager.Server, error) { serverManager := &servermanagerv1.ServerManager{} err := i.client.Get(ctx, client.ObjectKey{Namespace: i.Config.Namespace, Name: serverId}, serverManager) if err != nil { return nil, err } image, err := i.GetImage(ctx, serverManager.Spec.Server.Image) if err != nil { return nil, err } return &instancemanager.Server{ Id: serverManager.Name, Running: serverManager.Spec.Server.On, RunningCommand: strings.Join(serverManager.Status.Server.Args, " "), RunningImage: &models.Image{ Registry: image.Registry, Tag: image.Tag, Id: image.Id, }, }, nil } func (i *InstanceManager) ListServers(ctx context.Context) ([]instancemanager.Server, error) { serverManagers := &servermanagerv1.ServerManagerList{} err := i.client.List(ctx, serverManagers, &client.ListOptions{Namespace: i.Config.Namespace}) if err != nil { return nil, err } servers := make([]instancemanager.Server, len(serverManagers.Items)) for index, serverManager := range serverManagers.Items { image, err := i.GetImage(ctx, serverManager.Spec.Server.Image) if err != nil { return nil, err } ports := make([]models.Port, len(serverManager.Status.Server.HostPorts)) for i, port := range serverManager.Status.Server.HostPorts { ports[i] = models.Port{ Protocol: models.PortProtocol(port.Protocol), PublicPort: uint16(port.HostPort), ContainerPort: uint16(port.TargetPort), } } servers[index] = instancemanager.Server{ Id: serverManager.Name, Running: serverManager.Spec.Server.On, RunningCommand: strings.Join(serverManager.Status.Server.Args, " "), Ports: ports, Domain: serverManager.Status.Server.Domain, RunningImage: &models.Image{ Registry: image.Registry, Tag: image.Tag, Id: image.Id, }, } } return servers, nil } // State Changing func (i *InstanceManager) StartServer(ctx context.Context, serverId string, imageId string, command string, ports []models.Port) error { image := &servermanagerv1.Image{} err := i.client.Get(ctx, client.ObjectKey{Name: imageId, Namespace: i.Config.Namespace}, image) if err != nil { return err } serverManager := &servermanagerv1.ServerManager{} err = i.client.Get(ctx, client.ObjectKey{Name: serverId, Namespace: i.Config.Namespace}, serverManager) if err != nil { return err } serverManager.Spec.Server.Image = image.Name serverManager.Spec.Server.On = true serverManager.Spec.Server.Args, err = shellwords.Split(command) if err != nil { return err } err = i.client.Update(ctx, serverManager) if err != nil { return err } return nil } func (i *InstanceManager) StopServer(ctx context.Context, serverId string) error { serverManager := &servermanagerv1.ServerManager{} err := i.client.Get(ctx, client.ObjectKey{Name: serverId, Namespace: i.Config.Namespace}, serverManager) if err != nil { return err } serverManager.Spec.Server.On = false err = i.client.Update(ctx, serverManager) if err != nil { return err } return nil } func (i *InstanceManager) CreateServer(ctx context.Context) (*instancemanager.Server, error) { name := names.SimpleNameGenerator.GenerateName("") err := i.client.Create(ctx, &servermanagerv1.ServerManager{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: i.Config.Namespace, }, Spec: servermanagerv1.ServerManagerSpec{ Storage: "10Gi", Server: servermanagerv1.ServerSpec{ Image: "", On: false, }, }, }) if err != nil { return nil, err } serverManager := &instancemanager.Server{ Id: name, Running: false, RunningImage: nil, RunningCommand: "", } return serverManager, nil } func (i *InstanceManager) DeleteServer(ctx context.Context, serverId string) error { serverManager := &servermanagerv1.ServerManager{} err := i.client.Get(ctx, client.ObjectKey{Name: serverId, Namespace: i.Config.Namespace}, serverManager) if err != nil { if errors.IsNotFound(err) { return nil } else { return err } } err = i.client.Create(ctx, serverManager) if err != nil { return err } return nil } // Terminal // Status Changing func (i *InstanceManager) InteractiveTerminal(ctx context.Context, serverId string) (*instancemanager.TerminalConnection, error) { stdinReader, stdinWriter := io.Pipe() stdoutReader, stdoutWriter := io.Pipe() resizeChan := make(chan remotecommand.TerminalSize, 1) queue := &sizeQueue{resizeChan: resizeChan} req := i.coreV1Cli.CoreV1().RESTClient(). Post(). Resource("pods"). Namespace(i.Config.Namespace). Name(serverId). SubResource("attach"). VersionedParams(&corev1.PodAttachOptions{ Stdin: true, Stdout: true, Stderr: true, TTY: true, }, clientgoscheme.ParameterCodec) executor, err := remotecommand.NewSPDYExecutor(i.restCfg, "POST", req.URL()) go func() { defer stdoutWriter.Close() defer stdinReader.Close() _ = executor.StreamWithContext(ctx, remotecommand.StreamOptions{Stdin: stdinReader, Stdout: stdoutWriter, Stderr: stdoutWriter, Tty: true, TerminalSizeQueue: queue}) }() if err != nil { return nil, err } k := newKubeCon(stdinWriter, stdoutReader) return &instancemanager.TerminalConnection{ Conn: k, ResizerFunc: func(width uint, height uint) error { resizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)} return nil }, }, nil } // File Browser // Read Only func (i *InstanceManager) GetFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error) { serverManager := &servermanagerv1.ServerManager{} err := i.client.Get(ctx, client.ObjectKey{Namespace: i.Config.Namespace, Name: serverId}, serverManager) if err != nil { return nil, err } return &models.FileBrowser{ServerId: serverManager.Name, Id: serverManager.Name, Url: serverManager.Status.Browser.Url}, nil } func (i *InstanceManager) ListFileBrowsers(ctx context.Context) ([]models.FileBrowser, error) { serverManagers := &servermanagerv1.ServerManagerList{} err := i.client.List(ctx, serverManagers, &client.ListOptions{Namespace: i.Config.Namespace}) if err != nil { return nil, err } fileBrowsers := make([]models.FileBrowser, len(serverManagers.Items)) for i, serverManager := range serverManagers.Items { fileBrowsers[i] = models.FileBrowser{ServerId: serverManager.Name, Id: serverManager.Name, Url: serverManager.Status.Browser.Url} } return fileBrowsers, nil } // Status Changing func (i *InstanceManager) StartFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error) { serverManager := &servermanagerv1.ServerManager{} err := i.client.Get(ctx, client.ObjectKey{Namespace: i.Config.Namespace, Name: serverId}, serverManager) if err != nil { return nil, err } serverManager.Spec.Browser.On = true err = i.client.Update(ctx, serverManager) if err != nil { return nil, err } childContext, cancelFunc := context.WithTimeout(ctx, time.Second*30) defer cancelFunc() for !serverManager.Status.Browser.Running { err = i.client.Get(childContext, client.ObjectKey{Name: serverId, Namespace: i.Config.Namespace}, serverManager) if err != nil { return nil, err } } return &models.FileBrowser{Url: serverManager.Status.Browser.Url, Id: serverManager.Name, ServerId: serverManager.Name}, nil } func (i *InstanceManager) StopFileBrowser(ctx context.Context, serverId string) error { serverManager := &servermanagerv1.ServerManager{} err := i.client.Get(ctx, client.ObjectKey{Namespace: i.Config.Namespace, Name: serverId}, serverManager) if err != nil { return err } serverManager.Spec.Browser.On = false err = i.client.Update(ctx, serverManager) if err != nil { return err } return nil } func NewInstanceManager(config models.KubernetesInstanceManagerConfig) (*InstanceManager, error) { c, err := rest.InClusterConfig() if err != nil { kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") c, err = clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return nil, err } } scheme := runtime.NewScheme() clientgoscheme.AddToScheme(scheme) servermanagerv1.AddToScheme(scheme) client, err := client.New(c, client.Options{Scheme: scheme}) if err != nil { return nil, err } coreV1Cli, err := kubernetesclient.NewForConfig(c) if err != nil { return nil, err } return &InstanceManager{ client: client, coreV1Cli: coreV1Cli, restCfg: c, Config: config, }, nil }