diff --git a/cmd/podman/kube/down.go b/cmd/podman/kube/down.go index 9860992edd..79eb2e94a3 100644 --- a/cmd/podman/kube/down.go +++ b/cmd/podman/kube/down.go @@ -18,11 +18,11 @@ var ( Removes pods that have been based on the Kubernetes kind described in the YAML.` downCmd = &cobra.Command{ - Use: "down [options] KUBEFILE|-", + Use: "down [options] [KUBEFILE [KUBEFILE...]]|-", Short: "Remove pods based on Kubernetes YAML", Long: downDescription, RunE: down, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: common.AutocompleteDefaultOneArg, Example: `podman kube down nginx.yml cat nginx.yml | podman kube down - @@ -48,7 +48,7 @@ func downFlags(cmd *cobra.Command) { } func down(cmd *cobra.Command, args []string) error { - reader, err := readerFromArg(args[0]) + reader, err := readerFromArgs(args) if err != nil { return err } diff --git a/cmd/podman/kube/play.go b/cmd/podman/kube/play.go index 02b9572980..6783cc864c 100644 --- a/cmd/podman/kube/play.go +++ b/cmd/podman/kube/play.go @@ -51,11 +51,11 @@ var ( Creates pods or volumes based on the Kubernetes kind described in the YAML. Supported kinds are Pods, Deployments, DaemonSets, Jobs, and PersistentVolumeClaims.` playCmd = &cobra.Command{ - Use: "play [options] KUBEFILE|-", + Use: "play [options] [KUBEFILE [KUBEFILE...]]|-", Short: "Play a pod or volume based on Kubernetes YAML", Long: playDescription, RunE: play, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: common.AutocompleteDefaultOneArg, Example: `podman kube play nginx.yml cat nginx.yml | podman kube play - @@ -66,12 +66,12 @@ var ( var ( playKubeCmd = &cobra.Command{ - Use: "kube [options] KUBEFILE|-", + Use: "kube [options] [KUBEFILE [KUBEFILE...]]|-", Short: "Play a pod or volume based on Kubernetes YAML", Long: playDescription, Hidden: true, RunE: playKube, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: common.AutocompleteDefaultOneArg, Example: `podman play kube nginx.yml cat nginx.yml | podman play kube - @@ -276,7 +276,7 @@ func play(cmd *cobra.Command, args []string) error { return errors.New("--force may be specified only with --down") } - reader, err := readerFromArg(args[0]) + reader, err := readerFromArgs(args) if err != nil { return err } @@ -306,7 +306,7 @@ func play(cmd *cobra.Command, args []string) error { playOptions.ServiceContainer = true // Read the kube yaml file again so that a reader can be passed down to the teardown function - teardownReader, err = readerFromArg(args[0]) + teardownReader, err = readerFromArgs(args) if err != nil { return err } @@ -364,11 +364,43 @@ func playKube(cmd *cobra.Command, args []string) error { return play(cmd, args) } +func readerFromArgs(args []string) (*bytes.Reader, error) { + // if user tried to pipe, shortcut the reading + if len(args) == 1 && args[0] == "-" { + data, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil + } + + var combined bytes.Buffer + + for i, arg := range args { + reader, err := readerFromArg(arg) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + // Write the document content + combined.Write(data) + + // Only add `---` separator if it's not the last file + if i < len(args)-1 { + combined.WriteString("\n---\n") + } + } + return bytes.NewReader(combined.Bytes()), nil +} + func readerFromArg(fileName string) (*bytes.Reader, error) { var reader io.Reader switch { - case fileName == "-": // Read from stdin - reader = os.Stdin case parse.ValidWebURL(fileName) == nil: response, err := http.Get(fileName) if err != nil { diff --git a/cmd/podman/kube/play_test.go b/cmd/podman/kube/play_test.go new file mode 100644 index 0000000000..683bf1a1c9 --- /dev/null +++ b/cmd/podman/kube/play_test.go @@ -0,0 +1,161 @@ +package kube + +import ( + "io" + "os" + "strings" + "testing" +) + +// createTempFile writes content to a temp file and returns its path. +func createTempFile(t *testing.T, content string) string { + t.Helper() + + tmp, err := os.CreateTemp(t.TempDir(), "testfile-*.yaml") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + + if _, err := tmp.WriteString(content); err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } + + if err := tmp.Close(); err != nil { + t.Fatalf("failed to close temp file: %v", err) + } + + return tmp.Name() +} + +func TestReaderFromArgs(t *testing.T) { + tests := []struct { + name string + files []string // file contents + expected string // expected concatenated output + }{ + { + name: "single file", + files: []string{ + `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-config`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-config`, + }, + { + name: "two files", + files: []string{ + `apiVersion: v1 +kind: Pod +metadata: + name: my-pod`, + `apiVersion: v1 +kind: Service +metadata: + name: my-service`, + }, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: my-pod +--- +apiVersion: v1 +kind: Service +metadata: + name: my-service`, + }, + { + name: "empty file and normal file", + files: []string{ + ``, + `apiVersion: v1 +kind: Secret +metadata: + name: my-secret`, + }, + expected: `--- +apiVersion: v1 +kind: Secret +metadata: + name: my-secret`, + }, + { + name: "files with only whitespace", + files: []string{ + "\n \n", + `apiVersion: v1 +kind: Namespace +metadata: + name: test-ns`, + }, + expected: ` + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-ns`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var paths []string + for _, content := range tt.files { + path := createTempFile(t, content) + defer os.Remove(path) + paths = append(paths, path) + } + + reader, err := readerFromArgs(paths) + if err != nil { + t.Fatalf("readerFromArgs failed: %v", err) + } + + output, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to read result: %v", err) + } + + got := strings.TrimSpace(string(output)) + want := strings.TrimSpace(tt.expected) + + if got != want { + t.Errorf("unexpected output:\n--- got ---\n%s\n--- want ---\n%s", got, want) + } + }) + } +} + +func TestReaderFromArgs_Stdin(t *testing.T) { + const input = `apiVersion: v1 +kind: Namespace +metadata: + name: from-stdin` + + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + r, w, _ := os.Pipe() + _, _ = w.WriteString(input) + _ = w.Close() + os.Stdin = r + + reader, err := readerFromArgs([]string{"-"}) + if err != nil { + t.Fatalf("readerFromArgs failed: %v", err) + } + + data, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to read from stdin: %v", err) + } + + if got := string(data); got != input { + t.Errorf("unexpected stdin result:\n--- got ---\n%s\n--- want ---\n%s", got, input) + } +} diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 29dec3d434..2d060793c2 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -2350,6 +2350,46 @@ var _ = Describe("Podman kube play", func() { kubeYaml = filepath.Join(podmanTest.TempDir, "kube.yaml") }) + It("all arguments should be read", func() { + // let's create a matrix of pod name => filename + pods := [][]string{ + {"testPod1", "testPod1.yaml"}, + {"testPod2", "testPod2.yaml"}, + {"testPod3", "testPod3.yaml"}, + {"testPod4", "testPod4.yaml"}, + } + + // prepare our podman command + var cmd = []string{"play", "kube"} + + // for each pod: let's create a yaml file in the tmp dir & append the path to the cmd + for _, test := range pods { + // create the path + kubeYaml = filepath.Join(podmanTest.TempDir, test[1]) + + // add the path to our podman kube play command + cmd = append(cmd, kubeYaml) + + // generate a pod with a fake name + pod := getPod(withPodName(test[0])) + // write the yaml + err := generateKubeYaml("pod", pod, kubeYaml) + Expect(err).ToNot(HaveOccurred()) + } + + // run the podman command + kube := podmanTest.Podman(cmd) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(ExitCleanly()) + + // for each pods, let's ensure it has been created nicely + for _, test := range pods { + inspect := podmanTest.Podman([]string{"pod", "inspect", test[0]}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(ExitCleanly()) + } + }) + It("[play kube] fail with yaml of unsupported kind", func() { err := writeYaml(unknownKindYaml, kubeYaml) Expect(err).ToNot(HaveOccurred())