From d7292dbf2731701c122656400ce5007693efe3c8 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 15 Apr 2021 17:42:05 +0200 Subject: [PATCH] add --ip to podman play kube Add a new --ip flag to podman play kube. This is used to specify a static IP address which should be used for the pod. This option can be specified several times because play kube can create more than one pod. Fixes #8442 Signed-off-by: Paul Holzinger --- cmd/podman/play/kube.go | 4 +++ docs/source/markdown/podman-play-kube.1.md | 4 +++ pkg/api/handlers/libpod/play.go | 22 ++++++++++++--- pkg/api/server/register_play.go | 6 ++++ pkg/bindings/play/types.go | 4 +++ pkg/bindings/play/types_kube_options.go | 17 +++++++++++ pkg/domain/entities/play.go | 8 +++++- pkg/domain/infra/abi/play.go | 21 ++++++++++---- pkg/domain/infra/tunnel/play.go | 2 +- test/e2e/play_kube_test.go | 33 ++++++++++++++++++++++ 10 files changed, 110 insertions(+), 11 deletions(-) diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go index ddba5dc0f2..30d6d86f09 100644 --- a/cmd/podman/play/kube.go +++ b/cmd/podman/play/kube.go @@ -65,6 +65,10 @@ func init() { flags.StringVar(&kubeOptions.Network, networkFlagName, "", "Connect pod to CNI network(s)") _ = kubeCmd.RegisterFlagCompletionFunc(networkFlagName, common.AutocompleteNetworkFlag) + staticIPFlagName := "ip" + flags.IPSliceVar(&kubeOptions.StaticIPs, staticIPFlagName, nil, "Static IP addresses to assign to the pods") + _ = kubeCmd.RegisterFlagCompletionFunc(staticIPFlagName, completion.AutocompleteNone) + logDriverFlagName := "log-driver" flags.StringVar(&kubeOptions.LogDriver, logDriverFlagName, "", "Logging driver for the container") _ = kubeCmd.RegisterFlagCompletionFunc(logDriverFlagName, common.AutocompleteLogDriver) diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index 91899a8bd4..1074c27f86 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -62,6 +62,10 @@ The [username[:password]] to use to authenticate with the registry if required. If one or both values are not supplied, a command line prompt will appear and the value can be entered. The password is entered without echo. +#### **\-\-ip**=*IP address* + +Assign a static ip address to the pod. This option can be specified several times when play kube creates more than one pod. + #### **\-\-log-driver**=driver Set logging driver for all created containers. diff --git a/pkg/api/handlers/libpod/play.go b/pkg/api/handlers/libpod/play.go index eba5386b65..96f572a8b2 100644 --- a/pkg/api/handlers/libpod/play.go +++ b/pkg/api/handlers/libpod/play.go @@ -3,6 +3,7 @@ package libpod import ( "io" "io/ioutil" + "net" "net/http" "os" @@ -20,10 +21,11 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - Network string `schema:"network"` - TLSVerify bool `schema:"tlsVerify"` - LogDriver string `schema:"logDriver"` - Start bool `schema:"start"` + Network string `schema:"network"` + TLSVerify bool `schema:"tlsVerify"` + LogDriver string `schema:"logDriver"` + Start bool `schema:"start"` + StaticIPs []string `schema:"staticIPs"` }{ TLSVerify: true, Start: true, @@ -35,6 +37,17 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { return } + staticIPs := make([]net.IP, 0, len(query.StaticIPs)) + for _, ipString := range query.StaticIPs { + ip := net.ParseIP(ipString) + if ip == nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Errorf("Invalid IP address %s", ipString)) + return + } + staticIPs = append(staticIPs, ip) + } + // Fetch the K8s YAML file from the body, and copy it to a temp file. tmpfile, err := ioutil.TempFile("", "libpod-play-kube.yml") if err != nil { @@ -71,6 +84,7 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { Network: query.Network, Quiet: true, LogDriver: query.LogDriver, + StaticIPs: staticIPs, } if _, found := r.URL.Query()["tlsVerify"]; found { options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) diff --git a/pkg/api/server/register_play.go b/pkg/api/server/register_play.go index d21029db5f..da37abb70d 100644 --- a/pkg/api/server/register_play.go +++ b/pkg/api/server/register_play.go @@ -34,6 +34,12 @@ func (s *APIServer) registerPlayHandlers(r *mux.Router) error { // type: boolean // default: true // description: Start the pod after creating it. + // - in: query + // name: staticIPs + // type: array + // description: Static IPs used for the pods. + // items: + // type: string // - in: body // name: request // description: Kubernetes YAML file. diff --git a/pkg/bindings/play/types.go b/pkg/bindings/play/types.go index 5fb9a4d414..6598ec3c21 100644 --- a/pkg/bindings/play/types.go +++ b/pkg/bindings/play/types.go @@ -1,5 +1,7 @@ package play +import "net" + //go:generate go run ../generator/generator.go KubeOptions // KubeOptions are optional options for replaying kube YAML files type KubeOptions struct { @@ -23,6 +25,8 @@ type KubeOptions struct { // SeccompProfileRoot - path to a directory containing seccomp // profiles. SeccompProfileRoot *string + // StaticIPs - Static IP address used by the pod(s). + StaticIPs *[]net.IP // ConfigMaps - slice of pathnames to kubernetes configmap YAMLs. ConfigMaps *[]string // LogDriver for the container. For example: journald diff --git a/pkg/bindings/play/types_kube_options.go b/pkg/bindings/play/types_kube_options.go index 78396a090a..a1786f5536 100644 --- a/pkg/bindings/play/types_kube_options.go +++ b/pkg/bindings/play/types_kube_options.go @@ -1,6 +1,7 @@ package play import ( + "net" "net/url" "github.com/containers/podman/v3/pkg/bindings/internal/util" @@ -164,6 +165,22 @@ func (o *KubeOptions) GetSeccompProfileRoot() string { return *o.SeccompProfileRoot } +// WithStaticIPs +func (o *KubeOptions) WithStaticIPs(value []net.IP) *KubeOptions { + v := &value + o.StaticIPs = v + return o +} + +// GetStaticIPs +func (o *KubeOptions) GetStaticIPs() []net.IP { + var staticIPs []net.IP + if o.StaticIPs == nil { + return staticIPs + } + return *o.StaticIPs +} + // WithConfigMaps func (o *KubeOptions) WithConfigMaps(value []string) *KubeOptions { v := &value diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index cd8bb95069..c69bb08677 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -1,6 +1,10 @@ package entities -import "github.com/containers/image/v5/types" +import ( + "net" + + "github.com/containers/image/v5/types" +) // PlayKubeOptions controls playing kube YAML files. type PlayKubeOptions struct { @@ -24,6 +28,8 @@ type PlayKubeOptions struct { // SeccompProfileRoot - path to a directory containing seccomp // profiles. SeccompProfileRoot string + // StaticIPs - Static IP address used by the pod(s). + StaticIPs []net.IP // ConfigMaps - slice of pathnames to kubernetes configmap YAMLs. ConfigMaps []string // LogDriver for the container. For example: journald diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 52f759f132..d4c57bd07a 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/image" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgen/generate" "github.com/containers/podman/v3/pkg/specgen/generate/kube" "github.com/containers/podman/v3/pkg/util" @@ -50,6 +51,8 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return nil, errors.Wrapf(err, "unable to sort kube kinds in %q", path) } + ipIndex := 0 + // create pod on each document if it is a pod or deployment // any other kube kind will be skipped for _, document := range documentList { @@ -70,7 +73,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en podTemplateSpec.ObjectMeta = podYAML.ObjectMeta podTemplateSpec.Spec = podYAML.Spec - r, err := ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options) + r, err := ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options, &ipIndex) if err != nil { return nil, err } @@ -84,7 +87,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Deployment", path) } - r, err := ic.playKubeDeployment(ctx, &deploymentYAML, options) + r, err := ic.playKubeDeployment(ctx, &deploymentYAML, options, &ipIndex) if err != nil { return nil, err } @@ -118,7 +121,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return report, nil } -func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAML *v1apps.Deployment, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { +func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAML *v1apps.Deployment, options entities.PlayKubeOptions, ipIndex *int) (*entities.PlayKubeReport, error) { var ( deploymentName string podSpec v1.PodTemplateSpec @@ -140,7 +143,7 @@ func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAM // create "replicas" number of pods for i = 0; i < numReplicas; i++ { podName := fmt.Sprintf("%s-pod-%d", deploymentName, i) - podReport, err := ic.playKubePod(ctx, podName, &podSpec, options) + podReport, err := ic.playKubePod(ctx, podName, &podSpec, options, ipIndex) if err != nil { return nil, errors.Wrapf(err, "error encountered while bringing up pod %s", podName) } @@ -149,7 +152,7 @@ func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAM return &report, nil } -func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { +func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec, options entities.PlayKubeOptions, ipIndex *int) (*entities.PlayKubeReport, error) { var ( registryCreds *types.DockerAuthConfig writer io.Writer @@ -190,9 +193,17 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY // networks. networks := strings.Split(options.Network, ",") logrus.Debugf("Pod joining CNI networks: %v", networks) + p.NetNS.NSMode = specgen.Bridge p.CNINetworks = append(p.CNINetworks, networks...) } } + if len(options.StaticIPs) > *ipIndex { + p.StaticIP = &options.StaticIPs[*ipIndex] + *ipIndex++ + } else if len(options.StaticIPs) > 0 { + // only warn if the user has set at least one ip ip + logrus.Warn("No more static ips left using a random one") + } // Create the Pod pod, err := generate.MakePod(p, ic.Libpod) diff --git a/pkg/domain/infra/tunnel/play.go b/pkg/domain/infra/tunnel/play.go index 9f9076114f..e52e1a1f77 100644 --- a/pkg/domain/infra/tunnel/play.go +++ b/pkg/domain/infra/tunnel/play.go @@ -11,7 +11,7 @@ import ( func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, opts entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { options := new(play.KubeOptions).WithAuthfile(opts.Authfile).WithUsername(opts.Username).WithPassword(opts.Password) options.WithCertDir(opts.CertDir).WithQuiet(opts.Quiet).WithSignaturePolicy(opts.SignaturePolicy).WithConfigMaps(opts.ConfigMaps) - options.WithLogDriver(opts.LogDriver).WithNetwork(opts.Network).WithSeccompProfileRoot(opts.SeccompProfileRoot) + options.WithLogDriver(opts.LogDriver).WithNetwork(opts.Network).WithSeccompProfileRoot(opts.SeccompProfileRoot).WithStaticIPs(opts.StaticIPs) if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined { options.WithSkipTLSVerify(s == types.OptionalBoolTrue) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 41afd9f757..e006777bc9 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -12,6 +12,7 @@ import ( "github.com/containers/podman/v3/pkg/util" . "github.com/containers/podman/v3/test/utils" + "github.com/containers/storage/pkg/stringid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/opencontainers/selinux/go-selinux" @@ -1716,6 +1717,38 @@ spec: } }) + It("podman play kube --ip", func() { + var i, numReplicas int32 + numReplicas = 3 + deployment := getDeployment(withReplicas(numReplicas)) + err := generateKubeYaml("deployment", deployment, kubeYaml) + Expect(err).To(BeNil()) + + net := "playkube" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", "--subnet", "10.25.31.0/24", net}) + session.WaitWithDefaultTimeout() + defer podmanTest.removeCNINetwork(net) + Expect(session.ExitCode()).To(BeZero()) + + ips := []string{"10.25.31.5", "10.25.31.10", "10.25.31.15"} + playArgs := []string{"play", "kube", "--network", net} + for _, ip := range ips { + playArgs = append(playArgs, "--ip", ip) + } + + kube := podmanTest.Podman(append(playArgs, kubeYaml)) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + podNames := getPodNamesInDeployment(deployment) + for i = 0; i < numReplicas; i++ { + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(&podNames[i]), "--format", "{{ .NetworkSettings.Networks." + net + ".IPAddress }}"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(Equal(ips[i])) + } + }) + It("podman play kube test with network portbindings", func() { ip := "127.0.0.100" port := "5000"