Merge pull request #27292 from Honny1/pr-multi-file-support-kube

Add multi-file support to `podman kube play/down`
This commit is contained in:
openshift-merge-bot[bot]
2025-10-22 11:46:42 +00:00
committed by GitHub
5 changed files with 279 additions and 37 deletions

View File

@@ -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
}

View File

@@ -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
@@ -279,7 +281,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
}
@@ -309,7 +311,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
}
@@ -367,31 +369,54 @@ func playKube(cmd *cobra.Command, args []string) error {
return play(cmd, args)
}
func readerFromArg(fileName string) (*bytes.Reader, error) {
var reader io.Reader
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
}
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
}
if i < len(args)-1 {
// separate multiple files with YAML document separator
combined.WriteString(yamlFileSeparator)
}
}
return bytes.NewReader(combined.Bytes()), nil
}
func readerFromArg(fileOrURL string) (io.ReadCloser, error) {
switch {
case fileName == "-": // Read from stdin
reader = os.Stdin
case parse.ValidWebURL(fileName) == nil:
response, err := http.Get(fileName)
case parse.ValidWebURL(fileOrURL) == nil:
response, err := http.Get(fileOrURL)
if err != nil {
return nil, err
}
defer response.Body.Close()
reader = response.Body
return response.Body, nil
default:
f, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer f.Close()
reader = f
return os.Open(fileOrURL)
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return bytes.NewReader(data), nil
}
func teardown(body io.Reader, options entities.PlayKubeDownOptions) error {

View File

@@ -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)
}
}

View File

@@ -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)**

View File

@@ -2394,6 +2394,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())