diff --git a/cmd/podman/kube/play.go b/cmd/podman/kube/play.go index 2c6175af6f..46e69aa8cf 100644 --- a/cmd/podman/kube/play.go +++ b/cmd/podman/kube/play.go @@ -2,6 +2,7 @@ package kube import ( "bytes" + "context" "errors" "fmt" "io" @@ -9,9 +10,13 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "strings" "syscall" + v1 "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1" + "sigs.k8s.io/yaml" + buildahParse "github.com/containers/buildah/pkg/parse" "github.com/containers/common/pkg/auth" "github.com/containers/common/pkg/completion" @@ -174,6 +179,13 @@ func playFlags(cmd *cobra.Command) { flags.BoolVar(&playOptions.UseLongAnnotations, noTruncFlagName, false, "Use annotations that are not truncated to the Kubernetes maximum length of 63 characters") _ = flags.MarkHidden(noTruncFlagName) + buildFlagName := "build" + flags.BoolVar(&playOptions.BuildCLI, buildFlagName, false, "Build all images in a YAML (given Containerfiles exist)") + + contextDirFlagName := "context-dir" + flags.StringVar(&playOptions.ContextDir, contextDirFlagName, "", "Path to top level of context directory") + _ = cmd.RegisterFlagCompletionFunc(contextDirFlagName, completion.AutocompleteDefault) + if !registry.IsRemote() { certDirFlagName := "cert-dir" flags.StringVar(&playOptions.CertDir, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys") @@ -183,13 +195,6 @@ func playFlags(cmd *cobra.Command) { flags.StringVar(&playOptions.SeccompProfileRoot, seccompProfileRootFlagName, defaultSeccompRoot, "Directory path for seccomp profiles") _ = cmd.RegisterFlagCompletionFunc(seccompProfileRootFlagName, completion.AutocompleteDefault) - buildFlagName := "build" - flags.BoolVar(&playOptions.BuildCLI, buildFlagName, false, "Build all images in a YAML (given Containerfiles exist)") - - contextDirFlagName := "context-dir" - flags.StringVar(&playOptions.ContextDir, contextDirFlagName, "", "Path to top level of context directory") - _ = cmd.RegisterFlagCompletionFunc(contextDirFlagName, completion.AutocompleteDefault) - flags.StringVar(&playOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") _ = flags.MarkHidden("signature-policy") @@ -279,6 +284,22 @@ func play(cmd *cobra.Command, args []string) error { return err } + if registry.IsRemote() && playOptions.Build == types.OptionalBoolTrue { + var cwd string + if playOptions.ContextDir != "" { + cwd = playOptions.ContextDir + } else { + cwd = filepath.Dir(args[0]) + } + + result, err := kubeBuild(registry.ImageEngine(), registry.Context(), reader, cwd) + if err != nil { + return err + } + + reader = result + } + if playOptions.Down { return teardown(reader, entities.PlayKubeDownOptions{Force: playOptions.Force}) } @@ -358,6 +379,136 @@ func play(cmd *cobra.Command, args []string) error { return nil } +// Concatenate and create a bytes.Reader +func bytesArrayToReader(yamlArray [][]byte) *bytes.Reader { + // Use a buffer to concatenate the byte slices + var buffer bytes.Buffer + + // Loop through each []byte and add it to the buffer + for i, yamlDoc := range yamlArray { + buffer.Write(yamlDoc) + + // Add YAML document separator between documents, except after the last one + if i < len(yamlArray)-1 { + buffer.WriteString("\n---\n") + } + } + + // Return a bytes.Reader from the buffer + return bytes.NewReader(buffer.Bytes()) +} + +func kubeBuild(imageEngine entities.ImageEngine, context context.Context, body *bytes.Reader, contextDir string) (*bytes.Reader, error) { + content, err := io.ReadAll(body) + if err != nil { + return nil, err + } + if len(content) == 0 { + return nil, errors.New("yaml file provided is empty, cannot apply to a cluster") + } + + // Split the yaml file + documentList, err := util.SplitMultiDocYAML(content) + if err != nil { + return nil, err + } + + // sort kube kinds + documentList, err = util.SortKubeKinds(documentList) + if err != nil { + return nil, fmt.Errorf("unable to sort kube kinds: %w", err) + } + + output := make([][]byte, len(documentList)) + + for i, document := range documentList { + kind, err := util.GetKubeKind(document) + if err != nil { + return nil, fmt.Errorf("unable to read as kube YAML: %w", err) + } + + // ignore non-pod kind + if kind != "Pod" { + output[i] = document + continue + } + + var podYAML v1.Pod + if err := yaml.Unmarshal(document, &podYAML); err != nil { + return nil, fmt.Errorf("unable to read YAML as Kube Pod: %w", err) + } + + pod, err := podBuild(imageEngine, context, podYAML, contextDir) + if err != nil { + return nil, err + } + + // convert the pod object to bytes + podMarshaled, err := yaml.Marshal(&pod) + if err != nil { + return nil, err + } + + output[i] = podMarshaled + } + + return bytesArrayToReader(output), nil +} + +func podBuild(imageEngine entities.ImageEngine, context context.Context, pod v1.Pod, contextDir string) (*v1.Pod, error) { + for i := range pod.Spec.Containers { + // get the corresponding image + buildFile, err := util.GetBuildFile(pod.Spec.Containers[i].Image, contextDir) + if err != nil { + return nil, err + } + + found, err := imageEngine.Exists( + context, + pod.Spec.Containers[i].Image, + ) + if err != nil { + return nil, err + } + + if len(buildFile) == 0 { + continue + } + + if found.Value { + reports, _, err := imageEngine.Inspect(context, []string{pod.Spec.Containers[i].Image}, entities.InspectOptions{}) + if err != nil { + return nil, err + } + if len(reports) == 0 { + return nil, fmt.Errorf("image %s not found in %s", pod.Spec.Containers[i].Image, contextDir) + } + // overwrite the image id as container image + pod.Spec.Containers[i].Image = reports[0].ID + } else { + buildOpts := new(entities.BuildOptions) + buildOpts.Output = pod.Spec.Containers[i].Image + buildOpts.SystemContext = playOptions.SystemContext + buildOpts.ContextDirectory = filepath.Dir(buildFile) + + build, err := imageEngine.Build( + context, + []string{buildFile}, + *buildOpts, + ) + + if err != nil { + return nil, err + } + + // overwrite the image id as container image + pod.Spec.Containers[i].Image = build.ID + } + } + + return &pod, nil +} + func playKube(cmd *cobra.Command, args []string) error { return play(cmd, args) } diff --git a/cmd/podman/kube/play_test.go b/cmd/podman/kube/play_test.go new file mode 100644 index 0000000000..a783aabe33 --- /dev/null +++ b/cmd/podman/kube/play_test.go @@ -0,0 +1,101 @@ +package kube + +import ( + "context" + "io" + "testing" + + v1 "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1" + "github.com/stretchr/testify/assert" +) + +func TestPodBuild(t *testing.T) { + tests := []struct { + name string + pod v1.Pod + contextDir string + expectError bool + expectedErrorMsg string + expectedImages []string + }{ + { + "pod without containers should no raise any error", + v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{}, + }, + }, + "", + false, + "", + []string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + + pod, err := podBuild(nil, ctx, test.pod, test.contextDir) + if test.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectedErrorMsg) + } else { + assert.NoError(t, err) + for i, container := range pod.Spec.Containers { + assert.Equal(t, test.expectedImages[i], container.Image) + } + } + }) + } +} + +// TestBytesArrayToReader tests the bytesArrayToReader function +func TestBytesArrayToReader(t *testing.T) { + tests := []struct { + name string + input [][]byte + expected string + }{ + { + name: "single document", + input: [][]byte{ + []byte("document1"), + }, + expected: "document1", + }, + { + name: "two documents", + input: [][]byte{ + []byte("document1"), + []byte("document2"), + }, + expected: "document1\n---\ndocument2", + }, + { + name: "three documents", + input: [][]byte{ + []byte("document1"), + []byte("document2"), + []byte("document3"), + }, + expected: "document1\n---\ndocument2\n---\ndocument3", + }, + { + name: "empty input", + input: [][]byte{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := bytesArrayToReader(tt.input) + result, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assert.Equal(t, tt.expected, string(result)) + }) + } +} diff --git a/docs/source/markdown/podman-kube-play.1.md.in b/docs/source/markdown/podman-kube-play.1.md.in index a279f2b97f..38e462e8c1 100644 --- a/docs/source/markdown/podman-kube-play.1.md.in +++ b/docs/source/markdown/podman-kube-play.1.md.in @@ -68,8 +68,8 @@ A Kubernetes PersistentVolumeClaim represents a Podman named volume. Only the Pe Use `volume.podman.io/import-source` to import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) specified in the annotation's value into the created Podman volume -Kube play is capable of building images on the fly given the correct directory layout and Containerfiles. This -option is not available for remote clients, including Mac and Windows (excluding WSL2) machines, yet. Consider the following excerpt from a YAML file: +Kube play is capable of building images on the fly given the correct directory layout and Containerfiles. +Consider the following excerpt from a YAML file: ``` apiVersion: v1 kind: Pod @@ -178,7 +178,7 @@ An image can be automatically mounted into a container if the annotation `io.pod #### **--build** -Build images even if they are found in the local storage. Use `--build=false` to completely disable builds. (This option is not available with the remote Podman client) +Build images even if they are found in the local storage. Use `--build=false` to completely disable builds. Note: You can also override the default isolation type by setting the BUILDAH_ISOLATION environment variable. export BUILDAH_ISOLATION=oci. See podman-build.1.md for more information. @@ -193,7 +193,7 @@ The YAML file may be in a multi-doc YAML format. But, it must container only con #### **--context-dir**=*path* -Use *path* as the build context directory for each image. Requires --build option be true. (This option is not available with the remote Podman client) +Use *path* as the build context directory for each image. Requires --build option be true. @@option creds diff --git a/pkg/domain/infra/abi/apply.go b/pkg/domain/infra/abi/apply.go index 2c3986b4e1..f46656731a 100644 --- a/pkg/domain/infra/abi/apply.go +++ b/pkg/domain/infra/abi/apply.go @@ -14,6 +14,8 @@ import ( "os" "strings" + "github.com/containers/podman/v5/pkg/util" + "github.com/containers/podman/v5/pkg/domain/entities" k8sAPI "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1" "sigs.k8s.io/yaml" @@ -30,13 +32,13 @@ func (ic *ContainerEngine) KubeApply(ctx context.Context, body io.Reader, option } // Split the yaml file - documentList, err := splitMultiDocYAML(content) + documentList, err := util.SplitMultiDocYAML(content) if err != nil { return err } // Sort the kube kinds - documentList, err = sortKubeKinds(documentList) + documentList, err = util.SortKubeKinds(documentList) if err != nil { return fmt.Errorf("unable to sort kube kinds: %w", err) } @@ -60,7 +62,7 @@ func (ic *ContainerEngine) KubeApply(ctx context.Context, body io.Reader, option } for _, document := range documentList { - kind, err := getKubeKind(document) + kind, err := util.GetKubeKind(document) if err != nil { return fmt.Errorf("unable to read kube YAML: %w", err) } diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 5616a69f7a..ebf43e19eb 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -3,7 +3,6 @@ package abi import ( - "bytes" "context" "errors" "fmt" @@ -31,7 +30,6 @@ import ( "github.com/containers/podman/v5/pkg/domain/infra/abi/internal/expansion" v1apps "github.com/containers/podman/v5/pkg/k8s.io/api/apps/v1" v1 "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1" - metav1 "github.com/containers/podman/v5/pkg/k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/containers/podman/v5/pkg/specgen" "github.com/containers/podman/v5/pkg/specgen/generate" "github.com/containers/podman/v5/pkg/specgen/generate/kube" @@ -39,12 +37,10 @@ import ( "github.com/containers/podman/v5/pkg/systemd/notifyproxy" "github.com/containers/podman/v5/pkg/util" "github.com/containers/podman/v5/utils" - "github.com/containers/storage/pkg/fileutils" "github.com/coreos/go-systemd/v22/daemon" "github.com/opencontainers/go-digest" "github.com/opencontainers/selinux/go-selinux" "github.com/sirupsen/logrus" - yamlv3 "gopkg.in/yaml.v3" "sigs.k8s.io/yaml" ) @@ -264,13 +260,13 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, options } // split yaml document - documentList, err := splitMultiDocYAML(content) + documentList, err := util.SplitMultiDocYAML(content) if err != nil { return nil, err } // sort kube kinds - documentList, err = sortKubeKinds(documentList) + documentList, err = util.SortKubeKinds(documentList) if err != nil { return nil, fmt.Errorf("unable to sort kube kinds: %w", err) } @@ -302,7 +298,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, options // create pod on each document if it is a pod or deployment // any other kube kind will be skipped for _, document := range documentList { - kind, err := getKubeKind(document) + kind, err := util.GetKubeKind(document) if err != nil { return nil, fmt.Errorf("unable to read kube YAML: %w", err) } @@ -1174,7 +1170,7 @@ func (ic *ContainerEngine) getImageAndLabelInfo(ctx context.Context, cwd string, // Contains all labels obtained from kube labels := make(map[string]string) var pulledImage *libimage.Image - buildFile, err := getBuildFile(container.Image, cwd) + buildFile, err := util.GetBuildFile(container.Image, cwd) if err != nil { return nil, nil, err } @@ -1438,13 +1434,13 @@ func readConfigMapFromFile(r io.Reader) ([]v1.ConfigMap, error) { } // split yaml document - documentList, err := splitMultiDocYAML(content) + documentList, err := util.SplitMultiDocYAML(content) if err != nil { return nil, fmt.Errorf("unable to read as kube YAML: %w", err) } for _, document := range documentList { - kind, err := getKubeKind(document) + kind, err := util.GetKubeKind(document) if err != nil { return nil, fmt.Errorf("unable to read as kube YAML: %w", err) } @@ -1463,140 +1459,6 @@ func readConfigMapFromFile(r io.Reader) ([]v1.ConfigMap, error) { return configMaps, nil } -// splitMultiDocYAML reads multiple documents in a YAML file and -// returns them as a list. -func splitMultiDocYAML(yamlContent []byte) ([][]byte, error) { - var documentList [][]byte - - d := yamlv3.NewDecoder(bytes.NewReader(yamlContent)) - for { - var o interface{} - // read individual document - err := d.Decode(&o) - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("multi doc yaml could not be split: %w", err) - } - - if o == nil { - continue - } - - // back to bytes - document, err := yamlv3.Marshal(o) - if err != nil { - return nil, fmt.Errorf("individual doc yaml could not be marshalled: %w", err) - } - - kind, err := getKubeKind(document) - if err != nil { - return nil, fmt.Errorf("couldn't get object kind: %w", err) - } - - // The items in a document of kind "List" are fully qualified resources - // So, they can be treated as separate documents - if kind == "List" { - var kubeList metav1.List - if err := yaml.Unmarshal(document, &kubeList); err != nil { - return nil, err - } - for _, item := range kubeList.Items { - itemDocument, err := yamlv3.Marshal(item) - if err != nil { - return nil, fmt.Errorf("individual doc yaml could not be marshalled: %w", err) - } - - documentList = append(documentList, itemDocument) - } - } else { - documentList = append(documentList, document) - } - } - - return documentList, nil -} - -// getKubeKind unmarshals a kube YAML document and returns its kind. -func getKubeKind(obj []byte) (string, error) { - var kubeObject v1.ObjectReference - - if err := yaml.Unmarshal(obj, &kubeObject); err != nil { - return "", err - } - - return kubeObject.Kind, nil -} - -// sortKubeKinds adds the correct creation order for the kube kinds. -// Any pod dependency will be created first like volumes, secrets, etc. -func sortKubeKinds(documentList [][]byte) ([][]byte, error) { - var sortedDocumentList [][]byte - - for _, document := range documentList { - kind, err := getKubeKind(document) - if err != nil { - return nil, err - } - - switch kind { - case "Pod", "Deployment", "DaemonSet", "Job": - sortedDocumentList = append(sortedDocumentList, document) - default: - sortedDocumentList = append([][]byte{document}, sortedDocumentList...) - } - } - - return sortedDocumentList, nil -} - -func imageNamePrefix(imageName string) string { - prefix := imageName - s := strings.Split(prefix, ":") - if len(s) > 0 { - prefix = s[0] - } - s = strings.Split(prefix, "/") - if len(s) > 0 { - prefix = s[len(s)-1] - } - s = strings.Split(prefix, "@") - if len(s) > 0 { - prefix = s[0] - } - return prefix -} - -func getBuildFile(imageName string, cwd string) (string, error) { - buildDirName := imageNamePrefix(imageName) - containerfilePath := filepath.Join(cwd, buildDirName, "Containerfile") - dockerfilePath := filepath.Join(cwd, buildDirName, "Dockerfile") - - err := fileutils.Exists(containerfilePath) - if err == nil { - logrus.Debugf("Building %s with %s", imageName, containerfilePath) - return containerfilePath, nil - } - // If the error is not because the file does not exist, take - // a mulligan and try Dockerfile. If that also fails, return that - // error - if err != nil && !os.IsNotExist(err) { - logrus.Error(err.Error()) - } - - err = fileutils.Exists(dockerfilePath) - if err == nil { - logrus.Debugf("Building %s with %s", imageName, dockerfilePath) - return dockerfilePath, nil - } - // Strike two - if os.IsNotExist(err) { - return "", nil - } - return "", err -} - func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, body io.Reader, options entities.PlayKubeDownOptions) (*entities.PlayKubeReport, error) { var ( podNames []string @@ -1612,19 +1474,19 @@ func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, body io.Reader, opt } // split yaml document - documentList, err := splitMultiDocYAML(content) + documentList, err := util.SplitMultiDocYAML(content) if err != nil { return nil, err } // sort kube kinds - documentList, err = sortKubeKinds(documentList) + documentList, err = util.SortKubeKinds(documentList) if err != nil { return nil, fmt.Errorf("unable to sort kube kinds: %w", err) } for _, document := range documentList { - kind, err := getKubeKind(document) + kind, err := util.GetKubeKind(document) if err != nil { return nil, fmt.Errorf("unable to read as kube YAML: %w", err) } diff --git a/pkg/domain/infra/abi/play_test.go b/pkg/domain/infra/abi/play_test.go index 664d68a02c..83cda5dad4 100644 --- a/pkg/domain/infra/abi/play_test.go +++ b/pkg/domain/infra/abi/play_test.go @@ -166,115 +166,3 @@ data: }) } } - -func TestGetKubeKind(t *testing.T) { - tests := []struct { - name string - kubeYAML string - expectError bool - expectedErrorMsg string - expected string - }{ - { - "ValidKubeYAML", - ` -apiVersion: v1 -kind: Pod -`, - false, - "", - "Pod", - }, - { - "InvalidKubeYAML", - "InvalidKubeYAML", - true, - "cannot unmarshal", - "", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - kind, err := getKubeKind([]byte(test.kubeYAML)) - if test.expectError { - assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedErrorMsg) - } else { - assert.NoError(t, err) - assert.Equal(t, test.expected, kind) - } - }) - } -} - -func TestSplitMultiDocYAML(t *testing.T) { - tests := []struct { - name string - kubeYAML string - expectError bool - expectedErrorMsg string - expected int - }{ - { - "ValidNumberOfDocs", - ` -apiVersion: v1 -kind: Pod ---- -apiVersion: v1 -kind: Pod ---- -apiVersion: v1 -kind: Pod -`, - false, - "", - 3, - }, - { - "InvalidMultiDocYAML", - ` -apiVersion: v1 -kind: Pod ---- -apiVersion: v1 -kind: Pod -- -`, - true, - "multi doc yaml could not be split", - 0, - }, - { - "DocWithList", - ` -apiVersion: v1 -kind: List -items: -- apiVersion: v1 - kind: Pod -- apiVersion: v1 - kind: Pod -- apiVersion: v1 - kind: Pod -`, - false, - "", - 3, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - docs, err := splitMultiDocYAML([]byte(test.kubeYAML)) - if test.expectError { - assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedErrorMsg) - } else { - assert.NoError(t, err) - assert.Equal(t, test.expected, len(docs)) - } - }) - } -} diff --git a/pkg/machine/e2e/kube_test.go b/pkg/machine/e2e/kube_test.go new file mode 100644 index 0000000000..637b4cf887 --- /dev/null +++ b/pkg/machine/e2e/kube_test.go @@ -0,0 +1,61 @@ +package e2e_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +const ( + kube = ` +apiVersion: v1 +kind: Pod +metadata: + name: demo-build-remote +spec: + containers: + - name: container + image: foobar +` +) + +var _ = Describe("podman kube", func() { + It("play build", func() { + // init podman machine + name := randomString() + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withNow()).run() + Expect(err).ToNot(HaveOccurred()) + Expect(session).To(Exit(0)) + + // create a tmp directory + contextDir := GinkgoT().TempDir() + // create the yaml file which will be used + kubeFile := filepath.Join(contextDir, "kube.yaml") + err = os.WriteFile(kubeFile, []byte(kube), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // create the foobar directory + fooBarDir := filepath.Join(contextDir, "foobar") + err = os.Mkdir(fooBarDir, 0o755) + Expect(err).ToNot(HaveOccurred()) + + // create the Containerfile for the foorbar image + cfile := filepath.Join(fooBarDir, "Containerfile") + err = os.WriteFile(cfile, []byte("FROM quay.io/libpod/alpine_nginx\nRUN ip addr\n"), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // run the kube command with the build flag + bm := basicMachine{} + build, err := mb.setCmd(bm.withPodmanCommand([]string{"kube", "play", kubeFile, "--build"})).run() + Expect(err).ToNot(HaveOccurred()) + Expect(build).To(Exit(0)) + + output := build.outputToString() + Expect(output).To(ContainSubstring("Pod:")) + Expect(output).To(ContainSubstring("Container:")) + }) +}) diff --git a/pkg/util/kube.go b/pkg/util/kube.go index 7159fa7460..720f8e4331 100644 --- a/pkg/util/kube.go +++ b/pkg/util/kube.go @@ -1,5 +1,22 @@ package util +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "path/filepath" + "strings" + + v1 "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1" + metav1 "github.com/containers/podman/v5/pkg/k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/containers/storage/pkg/fileutils" + "github.com/sirupsen/logrus" + yamlv3 "gopkg.in/yaml.v3" + "sigs.k8s.io/yaml" +) + const ( // Kube annotation for podman volume driver. VolumeDriverAnnotation = "volume.podman.io/driver" @@ -18,3 +35,139 @@ const ( // Kube annotation for podman volume image. VolumeImageAnnotation = "volume.podman.io/image" ) + +// move it here ? + +// SplitMultiDocYAML reads multiple documents in a YAML file and +// returns them as a list. +func SplitMultiDocYAML(yamlContent []byte) ([][]byte, error) { + var documentList [][]byte + + d := yamlv3.NewDecoder(bytes.NewReader(yamlContent)) + for { + var o interface{} + // read individual document + err := d.Decode(&o) + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("multi doc yaml could not be split: %w", err) + } + + if o == nil { + continue + } + + // back to bytes + document, err := yamlv3.Marshal(o) + if err != nil { + return nil, fmt.Errorf("individual doc yaml could not be marshalled: %w", err) + } + + kind, err := GetKubeKind(document) + if err != nil { + return nil, fmt.Errorf("couldn't get object kind: %w", err) + } + + // The items in a document of kind "List" are fully qualified resources + // So, they can be treated as separate documents + if kind == "List" { + var kubeList metav1.List + if err := yaml.Unmarshal(document, &kubeList); err != nil { + return nil, err + } + for _, item := range kubeList.Items { + itemDocument, err := yamlv3.Marshal(item) + if err != nil { + return nil, fmt.Errorf("individual doc yaml could not be marshalled: %w", err) + } + + documentList = append(documentList, itemDocument) + } + } else { + documentList = append(documentList, document) + } + } + + return documentList, nil +} + +// GetKubeKind unmarshals a kube YAML document and returns its kind. +func GetKubeKind(obj []byte) (string, error) { + var kubeObject v1.ObjectReference + + if err := yaml.Unmarshal(obj, &kubeObject); err != nil { + return "", err + } + + return kubeObject.Kind, nil +} + +// SortKubeKinds adds the correct creation order for the kube kinds. +// Any pod dependency will be created first like volumes, secrets, etc. +func SortKubeKinds(documentList [][]byte) ([][]byte, error) { + var sortedDocumentList [][]byte + + for _, document := range documentList { + kind, err := GetKubeKind(document) + if err != nil { + return nil, err + } + + switch kind { + case "Pod", "Deployment", "DaemonSet", "Job": + sortedDocumentList = append(sortedDocumentList, document) + default: + sortedDocumentList = append([][]byte{document}, sortedDocumentList...) + } + } + + return sortedDocumentList, nil +} + +func imageNamePrefix(imageName string) string { + prefix := imageName + s := strings.Split(prefix, ":") + if len(s) > 0 { + prefix = s[0] + } + s = strings.Split(prefix, "/") + if len(s) > 0 { + prefix = s[len(s)-1] + } + s = strings.Split(prefix, "@") + if len(s) > 0 { + prefix = s[0] + } + return prefix +} + +func GetBuildFile(imageName string, cwd string) (string, error) { + buildDirName := imageNamePrefix(imageName) + containerfilePath := filepath.Join(cwd, buildDirName, "Containerfile") + dockerfilePath := filepath.Join(cwd, buildDirName, "Dockerfile") + + err := fileutils.Exists(containerfilePath) + if err == nil { + logrus.Debugf("Building %s with %s", imageName, containerfilePath) + return containerfilePath, nil + } + // If the error is not because the file does not exist, take + // a mulligan and try Dockerfile. If that also fails, return that + // error + if err != nil && !errors.Is(err, fs.ErrNotExist) { + logrus.Error(err.Error()) + } + + err = fileutils.Exists(dockerfilePath) + if err == nil { + logrus.Debugf("Building %s with %s", imageName, dockerfilePath) + return dockerfilePath, nil + } + // Strike two + if errors.Is(err, fs.ErrNotExist) { + return "", nil + } + return "", err +} diff --git a/pkg/util/kube_test.go b/pkg/util/kube_test.go new file mode 100644 index 0000000000..2810127648 --- /dev/null +++ b/pkg/util/kube_test.go @@ -0,0 +1,119 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetKubeKind(t *testing.T) { + tests := []struct { + name string + kubeYAML string + expectError bool + expectedErrorMsg string + expected string + }{ + { + "ValidKubeYAML", + ` +apiVersion: v1 +kind: Pod +`, + false, + "", + "Pod", + }, + { + "InvalidKubeYAML", + "InvalidKubeYAML", + true, + "cannot unmarshal", + "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + kind, err := GetKubeKind([]byte(test.kubeYAML)) + if test.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectedErrorMsg) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expected, kind) + } + }) + } +} + +func TestSplitMultiDocYAML(t *testing.T) { + tests := []struct { + name string + kubeYAML string + expectError bool + expectedErrorMsg string + expected int + }{ + { + "ValidNumberOfDocs", + ` +apiVersion: v1 +kind: Pod +--- +apiVersion: v1 +kind: Pod +--- +apiVersion: v1 +kind: Pod +`, + false, + "", + 3, + }, + { + "InvalidMultiDocYAML", + ` +apiVersion: v1 +kind: Pod +--- +apiVersion: v1 +kind: Pod +- +`, + true, + "multi doc yaml could not be split", + 0, + }, + { + "DocWithList", + ` +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod +- apiVersion: v1 + kind: Pod +- apiVersion: v1 + kind: Pod +`, + false, + "", + 3, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + docs, err := SplitMultiDocYAML([]byte(test.kubeYAML)) + if test.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectedErrorMsg) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expected, len(docs)) + } + }) + } +}