mirror of
https://github.com/containers/podman.git
synced 2025-11-29 01:28:22 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
149
cmd/podman/kube/play_test.go
Normal file
149
cmd/podman/kube/play_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)**
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user