From 5bbed82fdc0b535fade4e892ddb7084c9d1acdec Mon Sep 17 00:00:00 2001 From: Matthew Ingle Date: Sun, 4 Feb 2024 18:35:09 +0000 Subject: [PATCH] Number of fixes for SFTP File manager and Config File creation --- Dockerfile | 2 +- environment/kubernetes/pod.go | 172 ++++++++++++++++++++---------- parser/parser.go | 57 ++-------- server/filesystem/compress.go | 61 ++++++----- server/filesystem/filesystem.go | 76 +++++++------ server/filesystem/sftp_manager.go | 14 +-- server/install.go | 8 +- server/manager.go | 15 --- server/power.go | 15 --- 9 files changed, 211 insertions(+), 209 deletions(-) diff --git a/Dockerfile b/Dockerfile index eea10385..9496163e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app/ COPY go.mod go.sum /app/ RUN go mod download COPY . /app/ -RUN CGO_ENABLED=0 go build \ +RUN --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=0 go build \ -ldflags="-s -w -X github.com/kubectyl/kuber/system.Version=$VERSION" \ -v \ -trimpath \ diff --git a/environment/kubernetes/pod.go b/environment/kubernetes/pod.go index fdedc70d..4e719ecd 100644 --- a/environment/kubernetes/pod.go +++ b/environment/kubernetes/pod.go @@ -3,6 +3,8 @@ package kubernetes import ( "bufio" "context" + "encoding/base64" + "encoding/json" "fmt" "io" "os" @@ -23,6 +25,7 @@ import ( "github.com/kubectyl/kuber/config" "github.com/kubectyl/kuber/environment" + "github.com/kubectyl/kuber/parser" "github.com/kubectyl/kuber/system" corev1 "k8s.io/api/core/v1" @@ -409,45 +412,88 @@ func (e *Environment) Create() error { // Create a map to store the file data for the ConfigMap fileData := make(map[string]string) + fileReplaceOps := parser.FileReplaceOperations{} + for _, k := range cfs { - // replacement := make(map[string]string) + fileName := base64.URLEncoding.EncodeToString([]byte(k.FileName)) + fileName = strings.TrimRight(fileName, "=") for _, t := range k.Replace { - // replacement[t.Match] = t.ReplaceWith.String() - fileData[k.FileName] += fmt.Sprintf("%s=%s\n", t.Match, t.ReplaceWith.String()) + fileData[fileName] += fmt.Sprintf("%s=%s\n", t.Match, t.ReplaceWith.String()) } - - command, err := k.Parse("/config/", "/home/container/") - if err != nil { - return err + fileOp := parser.FileReplaceOperation{ + SourceFile: "/config/" + fileName, + TargetFile: "/home/container/" + k.FileName, + TargetType: k.Parser.String(), } - - // Add a new initContainer to the Pod - newInitContainer := corev1.Container{ - Name: "configuration-files", - Image: "busybox", - ImagePullPolicy: corev1.PullIfNotPresent, - SecurityContext: &corev1.SecurityContext{ - RunAsUser: pointer.Int64(1000), - RunAsNonRoot: pointer.Bool(true), + fileReplaceOps.Files = append(fileReplaceOps.Files, fileOp) + } + binaryFileOpData, err := json.Marshal(fileReplaceOps) + binData := make(map[string][]byte) + binData["config.json"] = binaryFileOpData + newConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: e.Id + "-config-replace-ops", + Namespace: cfg.Cluster.Namespace, + Labels: map[string]string{ + "Service": "Kubectyl", + "uuid": e.Id, }, - Command: command, - Resources: corev1.ResourceRequirements{}, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "replacement", - MountPath: "/config", - ReadOnly: true, - }, - { - Name: "storage", - MountPath: "/home/container", + }, + BinaryData: binData, + } + + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: "file-replace-ops", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: newConfigMap.Name, }, }, - } + }, + }) - pod.Spec.InitContainers = append(pod.Spec.InitContainers, newInitContainer) + err = e.CreateOrUpdateConfigMap(newConfigMap) + if err != nil { + return err } + // Add a new initContainer to the Pod + newInitContainer := corev1.Container{ + Name: "configuration-files", + Image: "inglemr/fileparser:latest", + ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: &corev1.SecurityContext{ + RunAsUser: pointer.Int64(1000), + RunAsNonRoot: pointer.Bool(true), + }, + Env: []corev1.EnvVar{ + { + Name: "CONFIG_LOCATION", + Value: "/fileparserconfig/config.json", + }, + }, + Resources: corev1.ResourceRequirements{}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "replacement", + MountPath: "/config", + ReadOnly: true, + }, + { + Name: "storage", + MountPath: "/home/container", + }, + { + Name: "file-replace-ops", + MountPath: "/fileparserconfig", + ReadOnly: true, + }, + }, + } + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, newInitContainer) + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ Name: "replacement", VolumeSource: corev1.VolumeSource{ @@ -464,28 +510,17 @@ func (e *Environment) Create() error { ObjectMeta: metav1.ObjectMeta{ Name: e.Id + "-replacement", Namespace: cfg.Cluster.Namespace, + Labels: map[string]string{ + "Service": "Kubectyl", + "uuid": e.Id, + }, }, Data: fileData, } - // Check if the ConfigMap already exists - _, err := e.client.CoreV1().ConfigMaps(cfg.Cluster.Namespace).Get(context.TODO(), e.Id+"-replacement", metav1.GetOptions{}) + err = e.CreateOrUpdateConfigMap(configMap) if err != nil { - if errors.IsNotFound(err) { - _, err = e.client.CoreV1().ConfigMaps(cfg.Cluster.Namespace).Create(context.TODO(), configMap, metav1.CreateOptions{}) - if err != nil { - return err - } - e.log().Info("replacement configmap created successfully") - } else { - return err - } - } else { - _, err = e.client.CoreV1().ConfigMaps(cfg.Cluster.Namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) - if err != nil { - return err - } - e.log().Info("replacement configmap updated successfully") + return err } } @@ -601,14 +636,12 @@ func (e *Environment) CreateService() error { Selector: map[string]string{ "uuid": e.Id, }, - Type: corev1.ServiceType(serviceType), - ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyType(externalPolicy), - HealthCheckNodePort: 0, - PublishNotReadyAddresses: true, - AllocateLoadBalancerNodePorts: new(bool), + Type: corev1.ServiceType(serviceType), + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyType(externalPolicy), + HealthCheckNodePort: 0, + PublishNotReadyAddresses: true, }, } - udp := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", @@ -626,15 +659,16 @@ func (e *Environment) CreateService() error { Selector: map[string]string{ "uuid": e.Id, }, - Type: corev1.ServiceType(serviceType), - ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyType(externalPolicy), - HealthCheckNodePort: 0, - PublishNotReadyAddresses: true, - AllocateLoadBalancerNodePorts: new(bool), + Type: corev1.ServiceType(serviceType), + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyType(externalPolicy), + HealthCheckNodePort: 0, + PublishNotReadyAddresses: true, }, } if serviceType == "LoadBalancer" && cfg.Cluster.MetalLBSharedIP { + udp.Spec.AllocateLoadBalancerNodePorts = new(bool) + tcp.Spec.AllocateLoadBalancerNodePorts = new(bool) tcp.Annotations = map[string]string{ "metallb.universe.tf/allow-shared-ip": e.Id, } @@ -1062,3 +1096,27 @@ func (e *Environment) convertMounts() ([]corev1.VolumeMount, []corev1.Volume) { return out, volumes } + +func (e *Environment) CreateOrUpdateConfigMap(configMap *corev1.ConfigMap) error { + // Check if the ConfigMap already exists + cfg := config.Get() + _, err := e.client.CoreV1().ConfigMaps(cfg.Cluster.Namespace).Get(context.TODO(), configMap.Name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + _, err = e.client.CoreV1().ConfigMaps(cfg.Cluster.Namespace).Create(context.TODO(), configMap, metav1.CreateOptions{}) + if err != nil { + return err + } + e.log().Info(configMap.Name + " configmap created successfully") + } else { + return err + } + } else { + _, err = e.client.CoreV1().ConfigMaps(cfg.Cluster.Namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) + if err != nil { + return err + } + e.log().Info(configMap.Name + " configmap updated successfully") + } + return nil +} diff --git a/parser/parser.go b/parser/parser.go index 22f11a77..5d0a64b7 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1,8 +1,6 @@ package parser import ( - "fmt" - "emperror.dev/errors" "github.com/apex/log" "github.com/buger/jsonparser" @@ -162,56 +160,21 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error { // in the API response from the Panel. func (f *ConfigurationFile) Parse(configDir, externalDir string) ([]string, error) { log.WithField("path", externalDir).WithField("parser", f.Parser.String()).Debug("parsing server configuration file") - if mb, err := json.Marshal(config.Get()); err != nil { return []string{}, err } else { f.configuration = mb } - switch f.Parser { - case Properties: - return []string{ - "/bin/sh", - "-c", - fmt.Sprintf(` - configDir="%s" - externalDir="%s" - - for configFilePath in $configDir*; do - filename=$(basename "$configFilePath") - externalFilePath="${externalDir}${filename}" - - if [ -f "$externalFilePath" ]; then - echo "Processing config file: $filename" - - while IFS='=' read -r key value; do - if [[ -n $key && $key != "#"* ]]; then - echo "Replacing $key with value $value in $externalFilePath" - if grep -qE "^$key=|^$key\s*=" "$externalFilePath"; then - sed -i "s|^$key=.*|$key=$value|g" "$externalFilePath" - else - echo "Invalid format in $externalFilePath" - fi - fi - done < "$configFilePath" - else - echo "File $externalFilePath not found" - fi - done - `, configDir, externalDir), - }, nil - case File: - return []string{}, nil - case Yaml, "yml": - return []string{}, nil - case Json: - return []string{}, nil - case Ini: - return []string{}, nil - case Xml: - return []string{}, nil - } - return []string{}, nil } + +type FileReplaceOperations struct { + Files []FileReplaceOperation `json:"files"` +} + +type FileReplaceOperation struct { + TargetFile string `json:"target_file"` + SourceFile string `json:"source_file"` + TargetType string `json:"target_type"` +} \ No newline at end of file diff --git a/server/filesystem/compress.go b/server/filesystem/compress.go index 196f7ba8..4b89e925 100644 --- a/server/filesystem/compress.go +++ b/server/filesystem/compress.go @@ -546,47 +546,56 @@ func (fs *Filesystem) extractBZIP2Archive(remoteFilePath string, destinationDir func (fs *Filesystem) extractZIPArchive(remoteFilePath string, destinationDir string) error { remoteFile, err := fs.manager.Open(remoteFilePath) if err != nil { - return fmt.Errorf("failed to open remote file: %v", err) + return fmt.Errorf("failed to open remote file: %w", err) } defer remoteFile.Close() - remoteFileInfo, err := fs.manager.Stat(remoteFilePath) + // Use the zip package to read the archive directly from the remote file. + // The zip.NewReader function requires the size, which we can obtain from the file's stat. + stat, err := remoteFile.Stat() if err != nil { - return err + return fmt.Errorf("failed to stat remote file: %w", err) } - zipReader, err := zip.NewReader(remoteFile, remoteFileInfo.Size()) + zipReader, err := zip.NewReader(remoteFile, stat.Size()) if err != nil { - return fmt.Errorf("failed to create zip reader: %v", err) + return fmt.Errorf("failed to create zip reader: %w", err) } for _, file := range zipReader.File { - destFilePath := filepath.Join(destinationDir, file.Name) - - if file.FileInfo().IsDir() { - err := fs.manager.MkdirAll(destFilePath) - if err != nil { - return fmt.Errorf("failed to create directory: %v", err) - } - continue + if err := fs.extractFileFromZip(file, destinationDir); err != nil { + return err // Error already formatted. } + } - destFile, err := fs.manager.Create(destFilePath) - if err != nil { - return fmt.Errorf("failed to create file: %v", err) - } - defer destFile.Close() + return nil +} - srcFile, err := fs.manager.Open(file.Name) - if err != nil { - return fmt.Errorf("failed to open file inside ZIP: %v", err) - } - defer srcFile.Close() +// Helper function to extract a single file from the zip archive. +func (fs *Filesystem) extractFileFromZip(file *zip.File, destinationDir string) error { + destFilePath := filepath.Join(destinationDir, file.Name) - _, err = io.Copy(destFile, srcFile) - if err != nil { - return fmt.Errorf("failed to extract file from ZIP: %v", err) + if file.FileInfo().IsDir() { + if err := fs.manager.MkdirAll(destFilePath); err != nil { + return fmt.Errorf("failed to create directory %s: %w", destFilePath, err) } + return nil + } + + destFile, err := fs.manager.Create(destFilePath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", destFilePath, err) + } + defer destFile.Close() + + srcFile, err := file.Open() + if err != nil { + return fmt.Errorf("failed to open file inside ZIP: %w", err) + } + defer srcFile.Close() + + if _, err = io.Copy(destFile, srcFile); err != nil { + return fmt.Errorf("failed to extract file %s from ZIP: %w", file.Name, err) } return nil diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index 41acde89..590938d2 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -130,29 +130,33 @@ func (fs *Filesystem) Touch(p string, flag int) (*sftp.File, error) { if err == nil { return f, nil } - // If the error is not because it doesn't exist then we just need to bail at this point. + + // If the error is not because the file doesn't exist, return the error. if !errors.Is(err, os.ErrNotExist) { return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle") } - // Only create and chown the directory if it doesn't exist. - if _, err := fs.manager.Stat(filepath.Dir(cleaned)); errors.Is(err, os.ErrNotExist) { - // Create the path leading up to the file we're trying to create, setting the final perms - // on it as we go. - if err := fs.manager.MkdirAll(filepath.Dir(cleaned)); err != nil { + + // At this point, the error is because the file does not exist. + // Ensure the directory exists and has the correct ownership. + dirPath := filepath.Dir(cleaned) + if _, err := fs.manager.Stat(dirPath); errors.Is(err, os.ErrNotExist) { + if err := fs.manager.MkdirAll(dirPath); err != nil { return nil, errors.Wrap(err, "server/filesystem: touch: failed to create directory tree") } - if err := fs.Chown(filepath.Dir(cleaned)); err != nil { + if err := fs.Chown(dirPath); err != nil { return nil, err } } - // o := &fileOpener{} - // Try to open the file now that we have created the pathing necessary for it, and then - // Chown that file so that the permissions don't mess with things. - // f, err = o.open(cleaned, flag, 0o644) - // if err != nil { - // return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file with wait") - // } - _ = fs.Chown(cleaned) + + // Attempt to create the file since it does not exist. + f, err = fs.manager.OpenFile(cleaned, flag|os.O_CREATE) + if err != nil { + return nil, errors.Wrap(err, "server/filesystem: touch: failed to create file") + } + + // Ensure the newly created file has the correct ownership. + _ = fs.Chown(cleaned) // Consider handling this error. + return f, nil } @@ -165,43 +169,37 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error { return err } - var currentSize int64 - // If the file does not exist on the system already go ahead and create the pathway - // to it and an empty file. We'll then write to it later on after this completes. + // Check if the file exists and is not a directory; no need to stat if we're going to overwrite. stat, err := fs.manager.Stat(cleaned) - if err != nil && !os.IsNotExist(err) { + if err == nil && stat.IsDir() { + return errors.WithStack(&Error{code: ErrCodeIsDirectory, resolved: cleaned}) + } else if err != nil && !os.IsNotExist(err) { return errors.Wrap(err, "server/filesystem: writefile: failed to stat file") - } else if err == nil { - if stat.IsDir() { - return errors.WithStack(&Error{code: ErrCodeIsDirectory, resolved: cleaned}) - } - currentSize = stat.Size() } - // br := bufio.NewReader(r) - // Check that the new size we're writing to the disk can fit. If there is currently - // a file we'll subtract that current file size from the size of the buffer to determine - // the amount of new data we're writing (or amount we're removing if smaller). - // if err := fs.HasSpaceFor(int64(br.Size()) - currentSize); err != nil { - // return err - // } - - // Touch the file and return the handle to it at this point. This will create the file, - // any necessary directories, and set the proper owner of the file. + // Touch the file to ensure it exists, directories are created, and it's ready for writing. + // This handles creating the file and truncating if it already exists. file, err := fs.Touch(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC) if err != nil { return err } defer file.Close() - buf := make([]byte, 1024*4) - sz, err := io.CopyBuffer(file, r, buf) + // Perform the write operation. + written, err := io.Copy(file, r) // io.Copy uses a 32KB buffer under the hood. + if err != nil { + return errors.Wrap(err, "server/filesystem: writefile: failed to write to file") + } - // Adjust the disk usage to account for the old size and the new size of the file. - fs.addDisk(sz - currentSize) + // Adjust the disk usage considering the new size. + if stat != nil { // If the file previously existed, adjust disk usage based on the difference. + fs.addDisk(written - stat.Size()) + } else { + fs.addDisk(written) // For a new file, just add the total written size. + } + // Set the proper ownership of the file after writing. return fs.Chown(cleaned) - // return nil } // Creates a new directory (name) at a specified path (p) for the server. diff --git a/server/filesystem/sftp_manager.go b/server/filesystem/sftp_manager.go index ef8bd16b..22c2df4d 100644 --- a/server/filesystem/sftp_manager.go +++ b/server/filesystem/sftp_manager.go @@ -196,12 +196,11 @@ func (m *BasicSFTPManager) Open(path string) (*sftp.File, error) { if err != nil { return nil, err } - - file, ok := result.(sftp.File) + file, ok := result.(*sftp.File) if !ok { return nil, errors.New("invalid file") } - return &file, err + return file, nil } func (m *BasicSFTPManager) Rename(oldname, newname string) error { @@ -219,11 +218,11 @@ func (m *BasicSFTPManager) Create(path string) (*sftp.File, error) { return nil, err } - file, ok := result.(sftp.File) + file, ok := result.(*sftp.File) if !ok { return nil, errors.New("invalid file") } - return &file, err + return file, err } func (m *BasicSFTPManager) ReadDir(p string) ([]fs.FileInfo, error) { @@ -259,11 +258,12 @@ func (m *BasicSFTPManager) OpenFile(path string, f int) (*sftp.File, error) { result, err := m.withConnectionCheck(func(client *sftp.Client) (interface{}, error) { return client.OpenFile(path, f) }) - file, ok := result.(sftp.File) + + file, ok := result.(*sftp.File) if !ok { return nil, errors.New("invalid file") } - return &file, err + return file, err } func (m *BasicSFTPManager) Chmod(path string, mode fs.FileMode) error { diff --git a/server/install.go b/server/install.go index 16b29850..9e0efa91 100644 --- a/server/install.go +++ b/server/install.go @@ -145,6 +145,7 @@ func (s *Server) internalInstall() error { if len(svc.Spec.Ports) > 0 { port = int(svc.Spec.Ports[0].NodePort) } + port = config.Get().System.Sftp.Port } s.fs.SetManager(fmt.Sprintf("%s:%v", ip, port)) @@ -498,6 +499,7 @@ func (ip *InstallationProcess) Execute() (string, error) { return "", err } + fsGroupChangePolicy := corev1.FSGroupChangeOnRootMismatch pod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", @@ -534,8 +536,10 @@ func (ip *InstallationProcess) Execute() (string, error) { }, }, SecurityContext: &corev1.PodSecurityContext{ - RunAsUser: pointer.Int64(1000), - RunAsNonRoot: pointer.Bool(true), + RunAsUser: pointer.Int64(1000), + RunAsNonRoot: pointer.Bool(true), + FSGroup: pointer.Int64(2000), + FSGroupChangePolicy: &fsGroupChangePolicy, }, Containers: []corev1.Container{ { diff --git a/server/manager.go b/server/manager.go index a0645108..0b7f767f 100644 --- a/server/manager.go +++ b/server/manager.go @@ -236,21 +236,6 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, ip := svc.Spec.ClusterIP port := config.Get().System.Sftp.Port - - switch svc.Spec.Type { - case "LoadBalancer": - if len(svc.Status.LoadBalancer.Ingress) > 0 { - ip = svc.Status.LoadBalancer.Ingress[0].IP - if len(svc.Spec.Ports) > 0 { - port = int(svc.Spec.Ports[0].Port) - } - } - case "NodePort": - if len(svc.Spec.Ports) > 0 { - port = int(svc.Spec.Ports[0].NodePort) - } - } - s.fs.SetManager(fmt.Sprintf("%s:%v", ip, port)) } } diff --git a/server/power.go b/server/power.go index bbf14b2b..d96fa2af 100644 --- a/server/power.go +++ b/server/power.go @@ -141,21 +141,6 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error ip := svc.Spec.ClusterIP port := config.Get().System.Sftp.Port - - switch svc.Spec.Type { - case "LoadBalancer": - if len(svc.Status.LoadBalancer.Ingress) > 0 { - ip = svc.Status.LoadBalancer.Ingress[0].IP - if len(svc.Spec.Ports) > 0 { - port = int(svc.Spec.Ports[0].Port) - } - } - case "NodePort": - if len(svc.Spec.Ports) > 0 { - port = int(svc.Spec.Ports[0].NodePort) - } - } - s.fs.SetManager(fmt.Sprintf("%s:%v", ip, port)) } }