From dcfa9828ae65ca253416439a2623a2fb21fa2d68 Mon Sep 17 00:00:00 2001 From: Ilya Lesikov Date: Wed, 18 May 2022 11:11:47 +0300 Subject: [PATCH] feat(kube-run): --copy-from-file and --copy-from-dir opts Signed-off-by: Ilya Lesikov --- cmd/werf/kube_run/kube_run.go | 571 ++++++++++++++++++++++++++++------ pkg/util/exec.go | 21 ++ pkg/util/strings.go | 22 ++ 3 files changed, 513 insertions(+), 101 deletions(-) create mode 100644 pkg/util/exec.go diff --git a/cmd/werf/kube_run/kube_run.go b/cmd/werf/kube_run/kube_run.go index 5423e3bca6..d2d8d9930b 100644 --- a/cmd/werf/kube_run/kube_run.go +++ b/cmd/werf/kube_run/kube_run.go @@ -1,13 +1,14 @@ package kube_run import ( + "bytes" "context" "encoding/base64" "encoding/json" "fmt" "math/rand" "os" - "os/exec" + "path" "path/filepath" "strings" @@ -15,6 +16,7 @@ import ( config2 "github.com/containers/image/v5/pkg/docker/config" imgtypes "github.com/containers/image/v5/types" "github.com/spf13/cobra" + "github.com/werf/werf/pkg/util" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -50,15 +52,22 @@ type cmdDataType struct { RmWithNamespace bool AutoPullSecret bool - Pod string - Command []string - ImageName string - Overrides string - ExtraOptions string + Pod string + Command []string + ImageName string + Overrides string + RunExtraOptions string + CopyFromFile []string + CopyFromDir []string registryCredsFound bool } +type copyFrom struct { + From string + To string +} + var ( cmdData cmdDataType commonCmdData common.CmdData @@ -79,14 +88,11 @@ func NewCmd() *cobra.Command { Short: "Run container for project image in Kubernetes", Long: common.GetLongCommandDescription(`Run container in Kubernetes for specified project image from werf.yaml (build if needed)`), DisableFlagsInUseLine: true, - Example: ` # Run specified image - $ werf kube-run --repo test/test application - - # Run interactive shell in the image + Example: ` # Run interactive shell in the image $ werf kube-run --repo test/test -it -- sh # Run image with specified command - $ werf kube-run --repo test/test -- /app/run.sh`, + $ werf kube-run --repo test/test application -- /app/run.sh`, Annotations: map[string]string{ common.DisableOptionsInUseLineAnno: "1", }, @@ -125,6 +131,14 @@ func NewCmd() *cobra.Command { } } + if err := validateCopyFromFile(); err != nil { + return fmt.Errorf("error validating --copy-from-file: %w", err) + } + + if err := validateCopyFromDir(); err != nil { + return fmt.Errorf("error validating --copy-from-dir: %w", err) + } + return runMain(ctx) }, } @@ -171,12 +185,14 @@ func NewCmd() *cobra.Command { cmd.Flags().StringVarP(&cmdData.Pod, "pod", "", os.Getenv("WERF_POD"), "Set created pod name (default $WERF_POD or autogenerated if not specified)") cmd.Flags().StringVarP(&cmdData.Overrides, "overrides", "", os.Getenv("WERF_OVERRIDES"), "Inline JSON to override/extend any fields in created Pod, e.g. to add imagePullSecrets field (default $WERF_OVERRIDES). %pod_name% and %container_name% will be replaced with names of a created pod and a container.") - cmd.Flags().StringVarP(&cmdData.ExtraOptions, "extra-options", "", os.Getenv("WERF_EXTRA_OPTIONS"), "Pass extra options to \"kubectl run\" command (default $WERF_EXTRA_OPTIONS)") + cmd.Flags().StringVarP(&cmdData.RunExtraOptions, "extra-options", "", os.Getenv("WERF_EXTRA_OPTIONS"), "Pass extra options to \"kubectl run\" command, which will create a Pod (default $WERF_EXTRA_OPTIONS)") cmd.Flags().BoolVarP(&cmdData.Rm, "rm", "", common.GetBoolEnvironmentDefaultTrue("WERF_RM"), "Remove pod and other created resources after command completion (default $WERF_RM or true if not specified)") cmd.Flags().BoolVarP(&cmdData.RmWithNamespace, "rm-with-namespace", "", common.GetBoolEnvironmentDefaultFalse("WERF_RM_WITH_NAMESPACE"), "Remove also a namespace after command completion (default $WERF_RM_WITH_NAMESPACE or false if not specified)") cmd.Flags().BoolVarP(&cmdData.Interactive, "interactive", "i", common.GetBoolEnvironmentDefaultFalse("WERF_INTERACTIVE"), "Enable interactive mode (default $WERF_INTERACTIVE or false if not specified)") cmd.Flags().BoolVarP(&cmdData.AllocateTty, "tty", "t", common.GetBoolEnvironmentDefaultFalse("WERF_TTY"), "Allocate a TTY (default $WERF_TTY or false if not specified)") cmd.Flags().BoolVarP(&cmdData.AutoPullSecret, "auto-pull-secret", "", common.GetBoolEnvironmentDefaultTrue("WERF_AUTO_PULL_SECRET"), "Automatically create docker config secret in the namespace and plug it via pod's imagePullSecrets for private registry access (default $WERF_AUTO_PULL_SECRET or true if not specified)") + cmd.Flags().StringArrayVarP(&cmdData.CopyFromFile, "copy-from-file", "", []string{}, "Copy file from container to local machine after execution (default $WERF_COPY_FROM_FILE). WARNING: parent directory will be copied to a temporary volume, which might affect performance. Example: \"/from/file:to\".") + cmd.Flags().StringArrayVarP(&cmdData.CopyFromDir, "copy-from-dir", "", []string{}, "Copy dir from container to local machine after execution (default $WERF_COPY_FROM_DIR). Example: \"/from/dir:to\".") return cmd } @@ -185,28 +201,22 @@ func processArgs(cmd *cobra.Command, args []string) error { doubleDashInd := cmd.ArgsLenAtDash() doubleDashExist := cmd.ArgsLenAtDash() != -1 - if doubleDashExist { - if doubleDashInd == len(args) { - return fmt.Errorf("unsupported position args format") - } + if !doubleDashExist { + return fmt.Errorf("-- should be specified") + } - switch doubleDashInd { - case 0: - cmdData.Command = args[doubleDashInd:] - case 1: - cmdData.ImageName = args[0] - cmdData.Command = args[doubleDashInd:] - default: - return fmt.Errorf("unsupported position args format") - } - } else { - switch len(args) { - case 0: - case 1: - cmdData.ImageName = args[0] - default: - return fmt.Errorf("unsupported position args format") - } + if doubleDashInd == len(args) { + return fmt.Errorf("unsupported position args format") + } + + switch doubleDashInd { + case 0: + cmdData.Command = args[doubleDashInd:] + case 1: + cmdData.ImageName = args[0] + cmdData.Command = args[doubleDashInd:] + default: + return fmt.Errorf("unsupported position args format") } return nil @@ -388,8 +398,11 @@ func run(ctx context.Context, pod, secret, namespace string, werfConfig *config. return err } + var dockerAuthConf imgtypes.DockerAuthConfig + var namedRef reference.Named if cmdData.AutoPullSecret { - namedRef, dockerAuthConf, err := getDockerConfigCredentials(image) + var err error + namedRef, dockerAuthConf, err = getDockerConfigCredentials(image) if err != nil { return fmt.Errorf("unable to get docker config credentials: %w", err) } @@ -397,94 +410,139 @@ func run(ctx context.Context, pod, secret, namespace string, werfConfig *config. if dockerAuthConf == (imgtypes.DockerAuthConfig{}) { logboek.Context(ctx).Debug().LogF("No credentials for werf repo found in Docker's config.json. No image pull secret will be created.\n") } else { - if err := createDockerRegistrySecret(ctx, secret, namespace, *namedRef, dockerAuthConf); err != nil { - return fmt.Errorf("unable to create docker registry secret: %w", err) - } cmdData.registryCredsFound = true } } - args := []string{ - "kubectl", - "run", - "--namespace", namespace, - pod, - "--image", image, - "--command", - "--restart", "Never", - "--quiet", - "--attach", + commonArgs, err := createCommonKubectlArgs(namespace) + if err != nil { + return fmt.Errorf("error creating common kubectl args: %w", err) } - if cmdData.Interactive { - args = append(args, "-i") + if err := createNamespace(ctx, namespace); err != nil { + return fmt.Errorf("unable to create namespace: %w", err) } - if cmdData.AllocateTty { - args = append(args, "-t") + if err := createDockerRegistrySecret(ctx, secret, namespace, namedRef, dockerAuthConf); err != nil { + return fmt.Errorf("unable to create docker registry secret: %w", err) + } + + return logboek.Streams().DoErrorWithoutProxyStreamDataFormatting(func() error { + return common.WithoutTerminationSignalsTrap(func() error { + if err := createPod(ctx, namespace, pod, image, secret, commonArgs); err != nil { + return fmt.Errorf("error creating Pod: %w", err) + } + + if err := waitPodReadiness(ctx, namespace, pod, commonArgs); err != nil { + return fmt.Errorf("error waiting for Pod readiness: %w", err) + } + + if err := execCommandInPod(ctx, namespace, pod, pod, cmdData.Command, commonArgs); err != nil { + return fmt.Errorf("error running command in Pod: %w", err) + } + + for _, copyFromFile := range getCopyFromFile() { + if err := copyFromPod(ctx, namespace, pod, pod, copyFromFile, commonArgs); err != nil { + return fmt.Errorf("error copying file from: %w", err) + } + } + + for _, copyFromDir := range getCopyFromDir() { + if err := copyFromPod(ctx, namespace, pod, pod, copyFromDir, commonArgs); err != nil { + return fmt.Errorf("error copying dir from: %w", err) + } + } + + if err := stopContainer(ctx, namespace, pod, pod, commonArgs); err != nil { + return fmt.Errorf("error stopping main container: %w", err) + } + + return nil + }) + }) +} + +func createCommonKubectlArgs(namespace string) ([]string, error) { + commonArgs := []string{ + "--namespace", namespace, } if *commonCmdData.KubeContext != "" { - args = append(args, "--context", *commonCmdData.KubeContext) + commonArgs = append(commonArgs, "--context", *commonCmdData.KubeContext) } if *commonCmdData.KubeConfigBase64 != "" { - args = append(args, "--kube-config-base64", *commonCmdData.KubeConfigBase64) + commonArgs = append(commonArgs, "--kube-config-base64", *commonCmdData.KubeConfigBase64) } else if *commonCmdData.KubeConfig != "" { if err := os.Setenv("KUBECONFIG", *commonCmdData.KubeConfig); err != nil { - return fmt.Errorf("unable to set $KUBECONFIG env var: %w", err) + return nil, fmt.Errorf("unable to set $KUBECONFIG env var: %w", err) } } else if len(*commonCmdData.KubeConfigPathMergeList) > 0 { if err := os.Setenv("KUBECONFIG", common.GetFirstExistingKubeConfigEnvVar()); err != nil { - return fmt.Errorf("unable to set $KUBECONFIG env var: %w", err) + return nil, fmt.Errorf("unable to set $KUBECONFIG env var: %w", err) } } - if overrides, err := generateOverrides(secret); err != nil { - return fmt.Errorf("error generating --overrides: %w", err) - } else if overrides != nil { - args = append(args, "--overrides", string(overrides), "--override-type", "strategic") - } + return commonArgs, nil +} - if cmdData.ExtraOptions != "" { - args = append(args, strings.Fields(cmdData.ExtraOptions)...) - } +func createPod(ctx context.Context, namespace, pod, image, secret string, extraArgs []string) error { + logboek.Context(ctx).LogF("Running pod %q in namespace %q ...\n", pod, namespace) - if len(cmdData.Command) > 0 { - args = append(args, "--") - args = append(args, cmdData.Command...) + args, err := createKubectlRunArgs(pod, image, secret, extraArgs) + if err != nil { + return fmt.Errorf("error creating kubectl run args: %w", err) } + cmd := util.ExecKubectlCmd(args...) + if *commonCmdData.DryRun { - fmt.Printf("werf %s\n", strings.Join(args, " ")) + fmt.Println(cmd.String()) return nil } - if err := createNamespace(ctx, namespace); err != nil { - return fmt.Errorf("unable to create namespace: %w", err) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error running pod: %w", err) } - return logboek.Streams().DoErrorWithoutProxyStreamDataFormatting(func() error { - return common.WithoutTerminationSignalsTrap(func() error { - logboek.Context(ctx).LogF("Running pod %q in namespace %q ...\n", pod, namespace) + return nil +} - cmd := exec.Command(strings.TrimSuffix(os.Args[0], "-in-a-user-namespace"), args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdin - cmd.Stdin = os.Stdin +func createKubectlRunArgs(pod string, image string, secret string, extraArgs []string) ([]string, error) { + args := []string{ + "run", + pod, + "--image", image, + "--command", + "--restart", "Never", + "--quiet", + "--pod-running-timeout=6h", + } - if err := cmd.Run(); err != nil { - return fmt.Errorf("error running pod: %w", err) - } + args = append(args, extraArgs...) - return nil - }) - }) + if overrides, err := generateOverrides(pod, image, secret); err != nil { + return nil, fmt.Errorf("error generating --overrides: %w", err) + } else if overrides != nil { + args = append(args, "--overrides", string(overrides), "--override-type", "strategic") + } + + if cmdData.RunExtraOptions != "" { + args = append(args, strings.Fields(cmdData.RunExtraOptions)...) + } + + args = append(args, "--") + args = append(args, "sh", "-euc", "until [ -f /tmp/werf-kube-run-quit ]; do sleep 1; done") + + return args, nil } // Can return nil overrides. -func generateOverrides(secret string) ([]byte, error) { - if (!cmdData.AutoPullSecret || !cmdData.registryCredsFound) && cmdData.Overrides == "" { +func generateOverrides(pod, image, secret string) ([]byte, error) { + if cmdData.Overrides == "" && + len(getCopyFromFile()) == 0 && + len(getCopyFromDir()) == 0 && + (!cmdData.AutoPullSecret || !cmdData.registryCredsFound) { return nil, nil } @@ -514,8 +572,183 @@ func generateOverrides(secret string) ([]byte, error) { return overrides, nil } +func cleanPodManifest(podJsonManifest []byte) ([]byte, error) { + podJsonManifest = []byte(strings.TrimSpace(string(podJsonManifest))) + + var pod map[string]interface{} + if err := json.Unmarshal(podJsonManifest, &pod); err != nil { + return nil, fmt.Errorf("error unmarshaling pod json manifest: %w", err) + } + + if pod["spec"].(map[string]interface{})["containers"] != nil { + return podJsonManifest, nil + } + + delete(pod["spec"].(map[string]interface{}), "containers") + if result, err := json.Marshal(pod); err != nil { + return nil, fmt.Errorf("error marshaling cleaned pod json manifest: %w", err) + } else { + return result, nil + } +} + +func waitPodReadiness(ctx context.Context, namespace, pod string, extraArgs []string) error { + if *commonCmdData.DryRun { + return nil + } + + logboek.Context(ctx).LogF("Waiting for pod %q in namespace %q to be ready ...\n", pod, namespace) + + for { + phase, err := getPodPhase(namespace, pod, extraArgs) + if err != nil { + return fmt.Errorf("error getting Pod phase: %w", err) + } + + switch phase { + case corev1.PodFailed: + return fmt.Errorf("pod %s/%s failed", namespace, pod) + case corev1.PodSucceeded: + return fmt.Errorf("pod %s/%s stopped too early", namespace, pod) + case corev1.PodRunning: + if ready, err := isPodReady(namespace, pod, extraArgs); err != nil { + return fmt.Errorf("error checking pod readiness: %w", err) + } else if ready { + return nil + } else { + continue + } + default: + continue + } + } +} + +func getPodPhase(namespace string, pod string, extraArgs []string) (corev1.PodPhase, error) { + args := []string{ + "get", "pod", "--template", "{{.status.phase}}", pod, + } + + args = append(args, extraArgs...) + + cmd := util.ExecKubectlCmd(args...) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("error getting pod %s/%s spec: %w", namespace, pod, err) + } + + return corev1.PodPhase(strings.TrimSpace(stdout.String())), nil +} + +func isPodReady(namespace string, pod string, extraArgs []string) (bool, error) { + args := []string{ + "get", "pod", "--template", "{{range .status.conditions}}{{if eq .type \"Ready\"}}{{.status}}{{end}}{{end}}", pod, + } + + args = append(args, extraArgs...) + + cmd := util.ExecKubectlCmd(args...) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + if err := cmd.Run(); err != nil { + return false, fmt.Errorf("error getting pod %s/%s spec: %w", namespace, pod, err) + } + + switch strings.TrimSpace(stdout.String()) { + case "True": + return true, nil + default: + return false, nil + } +} + +func copyFromPod(ctx context.Context, namespace, pod, container string, copyFrom copyFrom, extraArgs []string) error { + logboek.Context(ctx).LogF("Copying %q from pod %q in namespace %q to %q ...\n", copyFrom.From, pod, namespace, copyFrom.To) + + args := []string{ + "cp", fmt.Sprint(namespace, "/", pod, ":", copyFrom.From), copyFrom.To, "-c", container, + } + + args = append(args, extraArgs...) + + cmd := util.ExecKubectlCmd(args...) + + if *commonCmdData.DryRun { + fmt.Println(cmd.String()) + return nil + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("error copying %q from pod %s/%s: %w", copyFrom.From, namespace, pod, err) + } + + return nil +} + +func stopContainer(ctx context.Context, namespace, pod, container string, extraArgs []string) error { + logboek.Context(ctx).LogF("Stopping container %q in pod %q in namespace %q ...\n", container, pod, namespace) + + args := []string{ + "exec", pod, "-q", "--pod-running-timeout", "5h", "-c", container, + } + + args = append(args, extraArgs...) + args = append(args, "--", "touch", "/tmp/werf-kube-run-quit") + + cmd := util.ExecKubectlCmd(args...) + + if *commonCmdData.DryRun { + fmt.Println(cmd.String()) + return nil + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("error stopping service container %s/%s/%s for copying files: %w", namespace, pod, container, err) + } + return nil +} + +func execCommandInPod(ctx context.Context, namespace, pod, container string, command, extraArgs []string) error { + logboek.Context(ctx).LogF("Execing into pod %q in namespace %q ...\n", pod, namespace) + + args := []string{ + "exec", pod, "-q", "--pod-running-timeout", "5h", "-c", container, + } + + args = append(args, extraArgs...) + + if cmdData.Interactive { + args = append(args, "-i") + } + + if cmdData.AllocateTty { + args = append(args, "-t") + } + + args = append(args, "--") + args = append(args, command...) + + cmd := util.ExecKubectlCmd(args...) + + if *commonCmdData.DryRun { + fmt.Println(cmd.String()) + return nil + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("error exec'ing into pod %s/%s: %w", namespace, pod, err) + } + + return nil +} + func cleanupResources(ctx context.Context, pod, secret, namespace string) { - if !cmdData.Rm { + if !cmdData.Rm || *commonCmdData.DryRun { return } @@ -555,6 +788,10 @@ func cleanupResources(ctx context.Context, pod, secret, namespace string) { } func createNamespace(ctx context.Context, namespace string) error { + if *commonCmdData.DryRun { + return nil + } + if isNsExist, err := isNamespaceExist(ctx, namespace); err != nil { return fmt.Errorf("unable to check for namespace existence: %w", err) } else if isNsExist { @@ -563,7 +800,7 @@ func createNamespace(ctx context.Context, namespace string) error { logboek.Context(ctx).LogF("Creating namespace %q ...\n", namespace) - kube.Client.CoreV1().Namespaces().Create( + if _, err := kube.Client.CoreV1().Namespaces().Create( ctx, &corev1.Namespace{ ObjectMeta: v1.ObjectMeta{ @@ -571,12 +808,18 @@ func createNamespace(ctx context.Context, namespace string) error { }, }, v1.CreateOptions{}, - ) + ); err != nil { + return fmt.Errorf("error creating namespace %q: %w", namespace, err) + } return nil } func createDockerRegistrySecret(ctx context.Context, name, namespace string, ref reference.Named, dockerAuthConf imgtypes.DockerAuthConfig) error { + if *commonCmdData.DryRun || !cmdData.registryCredsFound { + return nil + } + secret := &corev1.Secret{ ObjectMeta: v1.ObjectMeta{ Name: name, @@ -610,7 +853,9 @@ func createDockerRegistrySecret(ctx context.Context, name, namespace string, ref secret.Data[corev1.DockerConfigJsonKey] = dockerConf logboek.Context(ctx).LogF("Creating secret %q in namespace %q ...\n", name, namespace) - kube.Client.CoreV1().Secrets(namespace).Create(ctx, secret, v1.CreateOptions{}) + if _, err := kube.Client.CoreV1().Secrets(namespace).Create(ctx, secret, v1.CreateOptions{}); err != nil { + return fmt.Errorf("error creating secret %s/%s: %w", namespace, secret, err) + } return nil } @@ -652,7 +897,7 @@ func isSecretExist(ctx context.Context, secret string, namespace string) (bool, } // Might return empty DockerAuthConfig. -func getDockerConfigCredentials(ref string) (*reference.Named, imgtypes.DockerAuthConfig, error) { +func getDockerConfigCredentials(ref string) (reference.Named, imgtypes.DockerAuthConfig, error) { namedRef, err := reference.ParseNormalizedNamed(ref) if err != nil { return nil, imgtypes.DockerAuthConfig{}, fmt.Errorf("unable to parse docker config registry reference %q: %w", ref, err) @@ -668,7 +913,7 @@ func getDockerConfigCredentials(ref string) (*reference.Named, imgtypes.DockerAu return nil, imgtypes.DockerAuthConfig{}, fmt.Errorf("unable to get docker registry creds for ref %q: %w", ref, err) } - return &namedRef, dockerAuthConf, nil + return namedRef, dockerAuthConf, nil } func addImagePullSecret(secret string, podOverrides *corev1.Pod) error { @@ -692,20 +937,144 @@ func templateOverrides(line, podName, containerName string) string { return strings.ReplaceAll(result, "%pod_name%", podName) } -func cleanPodManifest(podJsonManifest []byte) ([]byte, error) { - var pod map[string]interface{} - if err := json.Unmarshal(podJsonManifest, &pod); err != nil { - return nil, fmt.Errorf("error unmarshaling pod json manifest: %w", err) +func validateCopyFromFile() error { + rawCopyFrom := getCopyFromFileRaw() + + for _, cf := range rawCopyFrom { + parts := strings.Split(cf, ":") + if len(parts) != 2 { + return fmt.Errorf("wrong format: %s", cf) + } + + src := cleanCopyFromPodPath(parts[0]) + dst, err := cleanCopyFromLocalPath(parts[1], path.Base(src)) + if err != nil { + return fmt.Errorf("error cleaning destination path: %w", err) + } + + if strings.TrimSpace(src) == "" || strings.TrimSpace(dst) == "" { + return fmt.Errorf("invalid value: %s", cf) + } + + if !path.IsAbs(src) { + return fmt.Errorf("invalid value %q: source should be an absolute path", cf) + } + + if src == "/" { + return fmt.Errorf("invalid value %q: source root is not a file", cf) + } } - if pod["spec"].(map[string]interface{})["containers"] != nil { - return podJsonManifest, nil + return nil +} + +func validateCopyFromDir() error { + rawCopyFrom := getCopyFromDirRaw() + + for _, cf := range rawCopyFrom { + parts := strings.Split(cf, ":") + if len(parts) != 2 { + return fmt.Errorf("wrong format: %s", cf) + } + + src := cleanCopyFromPodPath(parts[0]) + dst, err := cleanCopyFromLocalPath(parts[1], path.Base(src)) + if err != nil { + return fmt.Errorf("error cleaning destination path: %w", err) + } + + if strings.TrimSpace(src) == "" || strings.TrimSpace(dst) == "" { + return fmt.Errorf("invalid value: %s", cf) + } + + if !path.IsAbs(src) { + return fmt.Errorf("invalid value %q: source should be an absolute path", cf) + } + + if src == "/" { + return fmt.Errorf("invalid value %q: source / is not allowed", cf) + } + + if path.Base(src) == "/" { + return fmt.Errorf("invalid value %q: source file can't be from root directory", cf) + } } - delete(pod["spec"].(map[string]interface{}), "containers") - if result, err := json.Marshal(pod); err != nil { - return nil, fmt.Errorf("error marshaling cleaned pod json manifest: %w", err) - } else { - return result, nil + return nil +} + +func getCopyFromFile() []copyFrom { + rawCopyFrom := getCopyFromFileRaw() + + var result []copyFrom + for _, rawcf := range rawCopyFrom { + parts := strings.Split(rawcf, ":") + src := cleanCopyFromPodPath(parts[0]) + dst, err := cleanCopyFromLocalPath(parts[1], path.Base(src)) + if err != nil { + panic("error cleaning destination path shouldn't happen") + } + + cf := copyFrom{ + From: src, + To: dst, + } + + result = append(result, cf) } + + return result +} + +func getCopyFromDir() []copyFrom { + rawCopyFrom := getCopyFromDirRaw() + + var result []copyFrom + for _, rawcf := range rawCopyFrom { + parts := strings.Split(rawcf, ":") + src := cleanCopyFromPodPath(parts[0]) + dst, err := cleanCopyFromLocalPath(parts[1], path.Base(src)) + if err != nil { + panic("error cleaning destination path shouldn't happen") + } + + cf := copyFrom{ + From: src, + To: dst, + } + + result = append(result, cf) + } + + return result +} + +func cleanCopyFromPodPath(rawPath string) string { + return filepath.ToSlash(filepath.Clean(rawPath)) +} + +func cleanCopyFromLocalPath(rawPath, srcBaseName string) (string, error) { + rawPath = filepath.Clean(rawPath) + + if rawPath == "." { + rawPath = filepath.Join(".", srcBaseName) + } + + rawPath = filepath.Clean(util.ExpandPath(rawPath)) + + var err error + rawPath, err = filepath.Abs(rawPath) + if err != nil { + return "", fmt.Errorf("error converting path %q to an absolute path: %w", rawPath, err) + } + + return rawPath, nil +} + +func getCopyFromFileRaw() []string { + return append(common.PredefinedValuesByEnvNamePrefix("WERF_COPY_FROM_FILE_"), cmdData.CopyFromFile...) +} + +func getCopyFromDirRaw() []string { + return append(common.PredefinedValuesByEnvNamePrefix("WERF_COPY_FROM_DIR_"), cmdData.CopyFromDir...) } diff --git a/pkg/util/exec.go b/pkg/util/exec.go new file mode 100644 index 0000000000..4110e0d126 --- /dev/null +++ b/pkg/util/exec.go @@ -0,0 +1,21 @@ +package util + +import ( + "os" + "os/exec" + "strings" +) + +func ExecWerfBinaryCmd(args ...string) *exec.Cmd { + cmd := exec.Command(strings.TrimSuffix(os.Args[0], "-in-a-user-namespace"), args...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdin + cmd.Stdin = os.Stdin + + return cmd +} + +func ExecKubectlCmd(args ...string) *exec.Cmd { + return ExecWerfBinaryCmd(append([]string{"kubectl"}, args...)...) +} diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 221d64465b..8bdeb210bd 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -62,6 +62,28 @@ outerLoop: return resultList } +func FilterSlice[V any](slice []V, filterFunc func(i int, val V) bool) []V { + var result []V + for i, val := range slice { + if filterFunc(i, val) { + result = append(result, val) + } + } + + return result +} + +// Returns nil if no match. +func FirstMatchInSliceIndex[V any](slice []V, matchFunc func(i int, val V) bool) *int { + for i := 0; i < len(slice); i++ { + if matchFunc(i, slice[i]) { + return &i + } + } + + return nil +} + func AddNewStringsToStringArray(list []string, elmsToAdd ...string) []string { outerLoop: for _, elmToAdd := range elmsToAdd {