Skip to content

Commit 9bda788

Browse files
axel7083Honny1
authored andcommitted
feat(cmd): podman kube play support multiple arguments
Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> fix: update kube play command cobra use Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> Add multi-file support to podman kube play/down - Support multiple YAML files and URLs in single command - Combine files with YAML document separators (---) - Refactor for better testability with dependency injection - Update documentation with examples for multiple inputs - Improve memory efficiency with streaming I/O operations Fixes: #26274 Fixes: https://issues.redhat.com/browse/RUN-3586 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent 2487242 commit 9bda788

File tree

5 files changed

+277
-35
lines changed

5 files changed

+277
-35
lines changed

cmd/podman/kube/down.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package kube
22

33
import (
4-
"github.com/containers/podman/v5/cmd/podman/common"
54
"github.com/containers/podman/v5/cmd/podman/registry"
65
"github.com/containers/podman/v5/cmd/podman/utils"
76
"github.com/containers/podman/v5/pkg/domain/entities"
87
"github.com/spf13/cobra"
8+
"go.podman.io/common/pkg/completion"
99
)
1010

1111
type downKubeOptions struct {
@@ -18,12 +18,12 @@ var (
1818
Removes pods that have been based on the Kubernetes kind described in the YAML.`
1919

2020
downCmd = &cobra.Command{
21-
Use: "down [options] KUBEFILE|-",
21+
Use: "down [options] [KUBEFILE [KUBEFILE...]]|-",
2222
Short: "Remove pods based on Kubernetes YAML",
2323
Long: downDescription,
2424
RunE: down,
25-
Args: cobra.ExactArgs(1),
26-
ValidArgsFunction: common.AutocompleteDefaultOneArg,
25+
Args: cobra.MinimumNArgs(1),
26+
ValidArgsFunction: completion.AutocompleteDefault,
2727
Example: `podman kube down nginx.yml
2828
cat nginx.yml | podman kube down -
2929
podman kube down https://example.com/nginx.yml`,
@@ -48,7 +48,7 @@ func downFlags(cmd *cobra.Command) {
4848
}
4949

5050
func down(_ *cobra.Command, args []string) error {
51-
reader, err := readerFromArg(args[0])
51+
reader, err := readerFromArgs(args)
5252
if err != nil {
5353
return err
5454
}

cmd/podman/kube/play.go

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type playKubeOptionsWrapper struct {
4242
macs []string
4343
}
4444

45+
const yamlFileSeparator = "\n---\n"
46+
4547
var (
4648
// https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/
4749
defaultSeccompRoot = "/var/lib/kubelet/seccomp"
@@ -51,12 +53,12 @@ var (
5153
Creates pods or volumes based on the Kubernetes kind described in the YAML. Supported kinds are Pods, Deployments, DaemonSets, Jobs, and PersistentVolumeClaims.`
5254

5355
playCmd = &cobra.Command{
54-
Use: "play [options] KUBEFILE|-",
56+
Use: "play [options] [KUBEFILE [KUBEFILE...]]|-",
5557
Short: "Play a pod or volume based on Kubernetes YAML",
5658
Long: playDescription,
5759
RunE: play,
58-
Args: cobra.ExactArgs(1),
59-
ValidArgsFunction: common.AutocompleteDefaultOneArg,
60+
Args: cobra.MinimumNArgs(1),
61+
ValidArgsFunction: completion.AutocompleteDefault,
6062
Example: `podman kube play nginx.yml
6163
cat nginx.yml | podman kube play -
6264
podman kube play --creds user:password --seccomp-profile-root /custom/path apache.yml
@@ -66,13 +68,13 @@ var (
6668

6769
var (
6870
playKubeCmd = &cobra.Command{
69-
Use: "kube [options] KUBEFILE|-",
71+
Use: "kube [options] [KUBEFILE [KUBEFILE...]]|-",
7072
Short: "Play a pod or volume based on Kubernetes YAML",
7173
Long: playDescription,
7274
Hidden: true,
7375
RunE: playKube,
74-
Args: cobra.ExactArgs(1),
75-
ValidArgsFunction: common.AutocompleteDefaultOneArg,
76+
Args: cobra.MinimumNArgs(1),
77+
ValidArgsFunction: completion.AutocompleteDefault,
7678
Example: `podman play kube nginx.yml
7779
cat nginx.yml | podman play kube -
7880
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 {
276278
return errors.New("--force may be specified only with --down")
277279
}
278280

279-
reader, err := readerFromArg(args[0])
281+
reader, err := readerFromArgs(args)
280282
if err != nil {
281283
return err
282284
}
@@ -306,7 +308,7 @@ func play(cmd *cobra.Command, args []string) error {
306308
playOptions.ServiceContainer = true
307309

308310
// Read the kube yaml file again so that a reader can be passed down to the teardown function
309-
teardownReader, err = readerFromArg(args[0])
311+
teardownReader, err = readerFromArgs(args)
310312
if err != nil {
311313
return err
312314
}
@@ -364,31 +366,54 @@ func playKube(cmd *cobra.Command, args []string) error {
364366
return play(cmd, args)
365367
}
366368

367-
func readerFromArg(fileName string) (*bytes.Reader, error) {
368-
var reader io.Reader
369-
switch {
370-
case fileName == "-": // Read from stdin
371-
reader = os.Stdin
372-
case parse.ValidWebURL(fileName) == nil:
373-
response, err := http.Get(fileName)
369+
func readerFromArgs(args []string) (*bytes.Reader, error) {
370+
return readerFromArgsWithStdin(args, os.Stdin)
371+
}
372+
373+
func readerFromArgsWithStdin(args []string, stdin io.Reader) (*bytes.Reader, error) {
374+
// if user tried to pipe, shortcut the reading
375+
if len(args) == 1 && args[0] == "-" {
376+
data, err := io.ReadAll(stdin)
374377
if err != nil {
375378
return nil, err
376379
}
377-
defer response.Body.Close()
378-
reader = response.Body
379-
default:
380-
f, err := os.Open(fileName)
380+
return bytes.NewReader(data), nil
381+
}
382+
383+
var combined bytes.Buffer
384+
385+
for i, arg := range args {
386+
reader, err := readerFromArg(arg)
387+
if err != nil {
388+
return nil, err
389+
}
390+
391+
_, err = io.Copy(&combined, reader)
392+
reader.Close()
381393
if err != nil {
382394
return nil, err
383395
}
384-
defer f.Close()
385-
reader = f
396+
397+
if i < len(args)-1 {
398+
// separate multiple files with YAML document separator
399+
combined.WriteString(yamlFileSeparator)
400+
}
386401
}
387-
data, err := io.ReadAll(reader)
388-
if err != nil {
389-
return nil, err
402+
403+
return bytes.NewReader(combined.Bytes()), nil
404+
}
405+
406+
func readerFromArg(fileOrURL string) (io.ReadCloser, error) {
407+
switch {
408+
case parse.ValidWebURL(fileOrURL) == nil:
409+
response, err := http.Get(fileOrURL)
410+
if err != nil {
411+
return nil, err
412+
}
413+
return response.Body, nil
414+
default:
415+
return os.Open(fileOrURL)
390416
}
391-
return bytes.NewReader(data), nil
392417
}
393418

394419
func teardown(body io.Reader, options entities.PlayKubeDownOptions) error {

cmd/podman/kube/play_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package kube
2+
3+
import (
4+
"io"
5+
"os"
6+
"strings"
7+
"testing"
8+
)
9+
10+
var configMapYAML = strings.Join([]string{
11+
"apiVersion: v1",
12+
"kind: ConfigMap",
13+
"metadata:",
14+
" name: my-config",
15+
"data:",
16+
" key: value",
17+
}, "\n")
18+
19+
var podYAML = strings.Join([]string{
20+
"apiVersion: v1",
21+
"kind: Pod",
22+
"metadata:",
23+
" name: my-pod",
24+
}, "\n")
25+
26+
var serviceYAML = strings.Join([]string{
27+
"apiVersion: v1",
28+
"kind: Service",
29+
"metadata:",
30+
" name: my-service",
31+
}, "\n")
32+
33+
var secretYAML = strings.Join([]string{
34+
"apiVersion: v1",
35+
"kind: Secret",
36+
"metadata:",
37+
" name: my-secret",
38+
}, "\n")
39+
40+
var namespaceYAML = strings.Join([]string{
41+
"apiVersion: v1",
42+
"kind: Namespace",
43+
"metadata:",
44+
" name: my-namespace",
45+
}, "\n")
46+
47+
// createTempFile writes content to a temp file and returns its path.
48+
func createTempFile(t *testing.T, content string) string {
49+
t.Helper()
50+
51+
tmp, err := os.CreateTemp(t.TempDir(), "testfile-*.yaml")
52+
if err != nil {
53+
t.Fatalf("failed to create temp file: %v", err)
54+
}
55+
56+
if _, err := tmp.WriteString(content); err != nil {
57+
t.Fatalf("failed to write to temp file: %v", err)
58+
}
59+
60+
if err := tmp.Close(); err != nil {
61+
t.Fatalf("failed to close temp file: %v", err)
62+
}
63+
64+
return tmp.Name()
65+
}
66+
67+
func TestReaderFromArgs(t *testing.T) {
68+
tests := []struct {
69+
name string
70+
files []string // file contents
71+
expected string // expected concatenated output
72+
}{
73+
{
74+
name: "single file",
75+
files: []string{configMapYAML},
76+
expected: configMapYAML,
77+
},
78+
{
79+
name: "two files",
80+
files: []string{
81+
podYAML,
82+
serviceYAML,
83+
},
84+
expected: podYAML + "\n---\n" + serviceYAML,
85+
},
86+
{
87+
name: "empty file and normal file",
88+
files: []string{
89+
"",
90+
secretYAML,
91+
},
92+
expected: "---\n" + secretYAML,
93+
},
94+
{
95+
name: "files with only whitespace",
96+
files: []string{
97+
"\n \n",
98+
namespaceYAML,
99+
},
100+
expected: "---\n" + namespaceYAML,
101+
},
102+
}
103+
104+
for _, tt := range tests {
105+
t.Run(tt.name, func(t *testing.T) {
106+
var paths []string
107+
for _, content := range tt.files {
108+
path := createTempFile(t, content)
109+
defer os.Remove(path)
110+
paths = append(paths, path)
111+
}
112+
113+
reader, err := readerFromArgsWithStdin(paths, nil)
114+
if err != nil {
115+
t.Fatalf("readerFromArgsWithStdin failed: %v", err)
116+
}
117+
118+
output, err := io.ReadAll(reader)
119+
if err != nil {
120+
t.Fatalf("failed to read result: %v", err)
121+
}
122+
123+
got := strings.TrimSpace(string(output))
124+
want := strings.TrimSpace(tt.expected)
125+
126+
if got != want {
127+
t.Errorf("unexpected output:\n--- got ---\n%s\n--- want ---\n%s", got, want)
128+
}
129+
})
130+
}
131+
}
132+
133+
func TestReaderFromArgs_Stdin(t *testing.T) {
134+
stdinReader := strings.NewReader(namespaceYAML)
135+
136+
reader, err := readerFromArgsWithStdin([]string{"-"}, stdinReader)
137+
if err != nil {
138+
t.Fatalf("readerFromArgsWithStdin failed: %v", err)
139+
}
140+
141+
data, err := io.ReadAll(reader)
142+
if err != nil {
143+
t.Fatalf("failed to read from stdin: %v", err)
144+
}
145+
146+
if got := string(data); got != namespaceYAML {
147+
t.Errorf("unexpected stdin result:\n--- got ---\n%s\n--- want ---\n%s", got, namespaceYAML)
148+
}
149+
}

docs/source/markdown/podman-kube-down.1.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
podman-kube-down - Remove containers and pods based on Kubernetes YAML
55

66
## SYNOPSIS
7-
**podman kube down** [*options*] *file.yml|-|https://website.io/file.yml*
7+
**podman kube down** [*options*] *file.yml|-|https://website.io/file.yml* [*file2.yml|https://website.io/file2.yml* ...]
88

99
## DESCRIPTION
10-
**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
11-
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
12-
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.
13-
`podman kube down` tears down the pods and containers created by `podman kube play` via the same Kubernetes YAML from the URL. However,
10+
**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
11+
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
12+
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.
13+
`podman kube down` tears down the pods and containers created by `podman kube play` via the same Kubernetes YAML from the URLs. However,
1414
`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
1515
`podman kube play`.
1616

17+
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`.
18+
1719
## OPTIONS
1820

1921
#### **--force**
@@ -67,5 +69,32 @@ Pods removed:
6769
`podman kube down` does not work with a URL if the YAML file the URL points to has been changed
6870
or altered since it was used to create the pods and containers.
6971

72+
Remove the pods and containers that were created from multiple YAML files
73+
```
74+
$ podman kube down pod.yml service.yml configmap.yml
75+
Pods stopped:
76+
52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6
77+
Pods removed:
78+
52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6
79+
```
80+
81+
Remove the pods and containers that were created from multiple URLs
82+
```
83+
$ podman kube down https://example.com/pod.yml https://example.com/service.yml https://example.com/configmap.yml
84+
Pods stopped:
85+
52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6
86+
Pods removed:
87+
52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6
88+
```
89+
90+
Remove the pods and containers that were created from a combination of local files and URLs
91+
```
92+
$ podman kube down local-pod.yml https://example.com/service.yml local-configmap.yml
93+
Pods stopped:
94+
52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6
95+
Pods removed:
96+
52182811df2b1e73f36476003a66ec872101ea59034ac0d4d3a7b40903b955a6
97+
```
98+
7099
## SEE ALSO
71100
**[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)**

0 commit comments

Comments
 (0)