diff --git a/cmd/podman/kube/down.go b/cmd/podman/kube/down.go index 853a81576d..3c3ac701f2 100644 --- a/cmd/podman/kube/down.go +++ b/cmd/podman/kube/down.go @@ -1,11 +1,11 @@ package kube import ( - "github.com/containers/podman/v5/cmd/podman/common" "github.com/containers/podman/v5/cmd/podman/registry" "github.com/containers/podman/v5/cmd/podman/utils" "github.com/containers/podman/v5/pkg/domain/entities" "github.com/spf13/cobra" + "go.podman.io/common/pkg/completion" ) type downKubeOptions struct { @@ -18,12 +18,12 @@ 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), - ValidArgsFunction: common.AutocompleteDefaultOneArg, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: completion.AutocompleteDefault, Example: `podman kube down nginx.yml cat nginx.yml | podman kube down - podman kube down https://example.com/nginx.yml`, @@ -48,7 +48,7 @@ func downFlags(cmd *cobra.Command) { } func down(_ *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 cfa6f39495..30b9eeb0d8 100644 --- a/cmd/podman/kube/play.go +++ b/cmd/podman/kube/play.go @@ -42,6 +42,8 @@ type playKubeOptionsWrapper struct { macs []string } +const yamlFileSeparator = "\n---\n" + var ( // https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/ defaultSeccompRoot = "/var/lib/kubelet/seccomp" @@ -51,12 +53,12 @@ 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), - ValidArgsFunction: common.AutocompleteDefaultOneArg, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: completion.AutocompleteDefault, Example: `podman kube play nginx.yml cat nginx.yml | podman kube play - podman kube play --creds user:password --seccomp-profile-root /custom/path apache.yml @@ -66,13 +68,13 @@ 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), - ValidArgsFunction: common.AutocompleteDefaultOneArg, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: completion.AutocompleteDefault, Example: `podman play kube nginx.yml cat nginx.yml | podman play kube - podman play kube --creds user:password --seccomp-profile-root /custom/path apache.yml @@ -276,7 +278,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 +308,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,31 +366,54 @@ func playKube(cmd *cobra.Command, args []string) error { return play(cmd, args) } -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) +func readerFromArgs(args []string) (*bytes.Reader, error) { + return readerFromArgsWithStdin(args, os.Stdin) +} + +func readerFromArgsWithStdin(args []string, stdin io.Reader) (*bytes.Reader, error) { + // if user tried to pipe, shortcut the reading + if len(args) == 1 && args[0] == "-" { + data, err := io.ReadAll(stdin) if err != nil { return nil, err } - defer response.Body.Close() - reader = response.Body - default: - f, err := os.Open(fileName) + return bytes.NewReader(data), nil + } + + var combined bytes.Buffer + + for i, arg := range args { + reader, err := readerFromArg(arg) + if err != nil { + return nil, err + } + + _, err = io.Copy(&combined, reader) + reader.Close() if err != nil { return nil, err } - defer f.Close() - reader = f + + if i < len(args)-1 { + // separate multiple files with YAML document separator + combined.WriteString(yamlFileSeparator) + } } - data, err := io.ReadAll(reader) - if err != nil { - return nil, err + + return bytes.NewReader(combined.Bytes()), nil +} + +func readerFromArg(fileOrURL string) (io.ReadCloser, error) { + switch { + case parse.ValidWebURL(fileOrURL) == nil: + response, err := http.Get(fileOrURL) + if err != nil { + return nil, err + } + return response.Body, nil + default: + return os.Open(fileOrURL) } - return bytes.NewReader(data), nil } func teardown(body io.Reader, options entities.PlayKubeDownOptions) error { diff --git a/cmd/podman/kube/play_test.go b/cmd/podman/kube/play_test.go new file mode 100644 index 0000000000..add7eeb671 --- /dev/null +++ b/cmd/podman/kube/play_test.go @@ -0,0 +1,149 @@ +package kube + +import ( + "io" + "os" + "strings" + "testing" +) + +var configMapYAML = strings.Join([]string{ + "apiVersion: v1", + "kind: ConfigMap", + "metadata:", + " name: my-config", + "data:", + " key: value", +}, "\n") + +var podYAML = strings.Join([]string{ + "apiVersion: v1", + "kind: Pod", + "metadata:", + " name: my-pod", +}, "\n") + +var serviceYAML = strings.Join([]string{ + "apiVersion: v1", + "kind: Service", + "metadata:", + " name: my-service", +}, "\n") + +var secretYAML = strings.Join([]string{ + "apiVersion: v1", + "kind: Secret", + "metadata:", + " name: my-secret", +}, "\n") + +var namespaceYAML = strings.Join([]string{ + "apiVersion: v1", + "kind: Namespace", + "metadata:", + " name: my-namespace", +}, "\n") + +// 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{configMapYAML}, + expected: configMapYAML, + }, + { + name: "two files", + files: []string{ + podYAML, + serviceYAML, + }, + expected: podYAML + "\n---\n" + serviceYAML, + }, + { + name: "empty file and normal file", + files: []string{ + "", + secretYAML, + }, + expected: "---\n" + secretYAML, + }, + { + name: "files with only whitespace", + files: []string{ + "\n \n", + namespaceYAML, + }, + expected: "---\n" + namespaceYAML, + }, + } + + 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 := readerFromArgsWithStdin(paths, nil) + if err != nil { + t.Fatalf("readerFromArgsWithStdin 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) { + stdinReader := strings.NewReader(namespaceYAML) + + reader, err := readerFromArgsWithStdin([]string{"-"}, stdinReader) + if err != nil { + t.Fatalf("readerFromArgsWithStdin 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 != namespaceYAML { + t.Errorf("unexpected stdin result:\n--- got ---\n%s\n--- want ---\n%s", got, namespaceYAML) + } +} diff --git a/docs/source/markdown/podman-kube-down.1.md b/docs/source/markdown/podman-kube-down.1.md index 14411e1a12..ae3a615833 100644 --- a/docs/source/markdown/podman-kube-down.1.md +++ b/docs/source/markdown/podman-kube-down.1.md @@ -4,16 +4,18 @@ podman-kube-down - Remove containers and pods based on Kubernetes YAML ## SYNOPSIS -**podman kube down** [*options*] *file.yml|-|https://website.io/file.yml* +**podman kube down** [*options*] *file.yml|-|https://website.io/file.yml* [*file2.yml|https://website.io/file2.yml* ...] ## DESCRIPTION -**podman kube down** reads a specified Kubernetes YAML file, tearing down pods that were created by the `podman kube play` command via the same Kubernetes YAML -file. Any volumes that were created by the previous `podman kube play` command remain intact unless the `--force` options is used. If the YAML file is -specified as `-`, `podman kube down` reads the YAML from stdin. The input can also be a URL that points to a YAML file such as https://podman.io/demo.yml. -`podman kube down` tears down the pods and containers created by `podman kube play` via the same Kubernetes YAML from the URL. However, +**podman kube down** reads one or more specified Kubernetes YAML files, tearing down pods that were created by the `podman kube play` command via the same Kubernetes YAML +files. Any volumes that were created by the previous `podman kube play` command remain intact unless the `--force` options is used. If the YAML file is +specified as `-`, `podman kube down` reads the YAML from stdin. The inputs can also be URLs that point to YAML files such as https://podman.io/demo.yml. +`podman kube down` tears down the pods and containers created by `podman kube play` via the same Kubernetes YAML from the URLs. However, `podman kube down` does not work with a URL if the YAML file the URL points to has been changed or altered since the creation of the pods and containers using `podman kube play`. +When multiple YAML files are specified (local files, URLs, or a combination), they are processed sequentially and combined with YAML document separators (`---`), just like with `podman kube play`. + ## OPTIONS #### **--force** @@ -67,5 +69,32 @@ Pods removed: `podman kube down` does not work with a URL if the YAML file the URL points to has been changed or altered since it was used to create the pods and containers. +Remove the pods and containers that were created from multiple YAML files +``` +$ podman kube down pod.yml service.yml configmap.yml +Pods stopped: +52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6 +Pods removed: +52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6 +``` + +Remove the pods and containers that were created from multiple URLs +``` +$ podman kube down https://example.com/pod.yml https://example.com/service.yml https://example.com/configmap.yml +Pods stopped: +52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6 +Pods removed: +52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6 +``` + +Remove the pods and containers that were created from a combination of local files and URLs +``` +$ podman kube down local-pod.yml https://example.com/service.yml local-configmap.yml +Pods stopped: +52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6 +Pods removed: +52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6 +``` + ## SEE ALSO **[podman(1)](podman.1.md)**, **[podman-kube(1)](podman-kube.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-kube-generate(1)](podman-kube-generate.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)** diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index c52ea71460..a2b3cb5780 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -2381,6 +2381,45 @@ var _ = Describe("Podman kube play", func() { kubeYaml = filepath.Join(podmanTest.TempDir, "kube.yaml") }) + It("all arguments should be read", func() { + pods := []string{"testPod1", "testPod2", "testPod3", "testPod4"} + cmd := []string{"kube", "play"} + + for _, name := range pods { + kubeYaml = filepath.Join(podmanTest.TempDir, name+".yaml") + + cmd = append(cmd, kubeYaml) + + pod := getPod(withPodName(name)) + + err := generateKubeYaml("pod", pod, kubeYaml) + Expect(err).ToNot(HaveOccurred()) + } + + podmanTest.PodmanExitCleanly(cmd...) + + ids := []string{} + for _, name := range pods { + inspect := podmanTest.PodmanExitCleanly( + "pod", "inspect", "--format", "{{.ID}}@{{.Name}}:{{.State}}", name, + ) + output := inspect.OutputToString() + id, state, found := strings.Cut(output, "@") + Expect(found).To(BeTrue()) + Expect(state).To(Equal(name + ":Running")) + ids = append(ids, id) + + } + + teardownCmd := []string{"kube", "down"} + teardownCmd = append(teardownCmd, cmd[2:]...) + teardown := podmanTest.PodmanExitCleanly(teardownCmd...) + teardownOutput := teardown.OutputToString() + for _, id := range ids { + Expect(teardownOutput).Should(ContainSubstring(id)) + } + }) + It("[play kube] fail with yaml of unsupported kind", func() { err := writeYaml(unknownKindYaml, kubeYaml) Expect(err).ToNot(HaveOccurred())