diff --git a/cmd/podman/kube/apply.go b/cmd/podman/kube/apply.go new file mode 100644 index 0000000000..d075b51123 --- /dev/null +++ b/cmd/podman/kube/apply.go @@ -0,0 +1,111 @@ +package kube + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v4/cmd/podman/common" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/utils" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + applyOptions = entities.ApplyOptions{} + applyDescription = `Command applies a podman container, pod, volume, or kube yaml to a Kubernetes cluster when a kubeconfig file is given.` + + applyCmd = &cobra.Command{ + Use: "apply [options] [CONTAINER...|POD...|VOLUME...]", + Short: "Deploy a podman container, pod, volume, or Kubernetes yaml to a Kubernetes cluster", + Long: applyDescription, + RunE: apply, + ValidArgsFunction: common.AutocompleteForKube, + Example: `podman kube apply ctrName volName + podman kube apply --namespace project -f fileName`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: applyCmd, + Parent: kubeCmd, + }) + applyFlags(applyCmd) +} + +func applyFlags(cmd *cobra.Command) { + flags := cmd.Flags() + flags.SetNormalizeFunc(utils.AliasFlags) + + kubeconfigFlagName := "kubeconfig" + flags.StringVarP(&applyOptions.Kubeconfig, kubeconfigFlagName, "k", os.Getenv("KUBECONFIG"), "Path to the kubeconfig file for the Kubernetes cluster") + _ = cmd.RegisterFlagCompletionFunc(kubeconfigFlagName, completion.AutocompleteDefault) + + namespaceFlagName := "ns" + flags.StringVarP(&applyOptions.Namespace, namespaceFlagName, "", "", "The namespace to deploy the workload to on the Kubernetes cluster") + _ = cmd.RegisterFlagCompletionFunc(namespaceFlagName, completion.AutocompleteNone) + + caCertFileFlagName := "ca-cert-file" + flags.StringVarP(&applyOptions.CACertFile, caCertFileFlagName, "", "", "Path to the CA cert file for the Kubernetes cluster.") + _ = cmd.RegisterFlagCompletionFunc(caCertFileFlagName, completion.AutocompleteDefault) + + fileFlagName := "file" + flags.StringVarP(&applyOptions.File, fileFlagName, "f", "", "Path to the Kubernetes yaml file to deploy.") + _ = cmd.RegisterFlagCompletionFunc(fileFlagName, completion.AutocompleteDefault) + + serviceFlagName := "service" + flags.BoolVarP(&applyOptions.Service, serviceFlagName, "s", false, "Create a service object for the container being deployed.") +} + +func apply(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("file") && cmd.Flags().Changed("service") { + return errors.New("cannot set --service and --file at the same time") + } + + kubeconfig, err := cmd.Flags().GetString("kubeconfig") + if err != nil { + return err + } + if kubeconfig == "" { + return errors.New("kubeconfig not given, unable to connect to cluster") + } + + var reader io.Reader + if cmd.Flags().Changed("file") { + yamlFile := applyOptions.File + if yamlFile == "-" { + yamlFile = os.Stdin.Name() + } + + f, err := os.Open(yamlFile) + if err != nil { + return err + } + defer f.Close() + reader = f + } else { + generateOptions.Service = applyOptions.Service + report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args, generateOptions) + if err != nil { + return err + } + if r, ok := report.Reader.(io.ReadCloser); ok { + defer r.Close() + } + reader = report.Reader + } + + fmt.Println("Deploying to cluster...") + + if err = registry.ContainerEngine().KubeApply(registry.GetContext(), reader, applyOptions); err != nil { + return err + } + + fmt.Println("Successfully deployed workloads to cluster!") + + return nil +} diff --git a/docs/source/markdown/podman-kube-apply.1.md b/docs/source/markdown/podman-kube-apply.1.md new file mode 100644 index 0000000000..c71e6dc42c --- /dev/null +++ b/docs/source/markdown/podman-kube-apply.1.md @@ -0,0 +1,73 @@ +-% podman-kube-apply(1) +## NAME +podman-kube-apply - Apply Kubernetes YAML based on containers, pods, or volumes to a Kubernetes cluster + +## SYNOPSIS +**podman kube apply** [*options*] [*container...* | *pod...* | *volume...*] + +## DESCRIPTION +**podman kube apply** will deploy a podman container, pod, or volume to a Kubernetes cluster. Use the `--file` flag to deploy a Kubernetes YAML (v1 specification) to a kubernetes cluster as well. + +Note that the Kubernetes YAML file can be used to run the deployment in Podman via podman-play-kube(1). + +## OPTIONS + +#### **--ca-cert-file**=*ca cert file path | "insecure"* + +The path to the CA cert file for the Kubernetes cluster. Usually the kubeconfig has the CA cert file data and `generate kube` automatically picks that up if it is available in the kubeconfig. If no CA cert file data is available, set this to `insecure` to bypass the certificate verification. + +#### **--file**, **-f**=*kube yaml filepath* + +Path to the kubernetes yaml file to deploy onto the kubernetes cluster. This file can be generated using the `podman kube generate` command. The input may be in the form of a yaml file, or stdin. For stdin, use `--file=-`. + +#### **--kubeconfig**, **-k**=*kubeconfig filepath* + +Path to the kubeconfig file to be used when deploying the generated kube yaml to the Kubernetes cluster. The environment variable `KUBECONFIG` can be used to set the path for the kubeconfig file as well. +Note: A kubeconfig can have multiple cluster configurations, but `kube generate` will always only pick the first cluster configuration in the given kubeconfig. + +#### **--ns**=*namespace* + +The namespace or project to deploy the workloads of the generated kube yaml to in the Kubernetes cluster. + +#### **--service**, **-s** + +Used to create a service for the corresponding container or pod being deployed to the cluster. In particular, if the container or pod has portmap bindings, the service specification will include a NodePort declaration to expose the service. A random port is assigned by Podman in the service specification that is deployed to the cluster. + +## EXAMPLES + +Apply a podman volume and container to the "default" namespace in a Kubernetes cluster. +``` +$ podman kube apply --kubeconfig /tmp/kubeconfig myvol vol-test-1 +Deploying to cluster... +Successfully deployed workloads to cluster! +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +vol-test-1-pod 1/1 Running 0 9m +``` + +Apply a Kubernetes YAML file to the "default" namespace in a Kubernetes cluster. +``` +$ podman kube apply --kubeconfig /tmp/kubeconfig -f vol.yaml +Deploying to cluster... +Successfully deployed workloads to cluster! +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +vol-test-2-pod 1/1 Running 0 9m +``` + +Apply a Kubernetes YAML file to the "test1" namespace in a Kubernetes cluster. +``` +$ podman kube apply --kubeconfig /tmp/kubeconfig --ns test1 vol-test-3 +Deploying to cluster... +Successfully deployed workloads to cluster! +$ kubectl get pods --namespace test1 +NAME READY STATUS RESTARTS AGE +vol-test-3-pod 1/1 Running 0 9m + +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-kube-generate(1)](podman-kube-generate.1.md)** + +## HISTORY +September 2022, Originally compiled by Urvashi Mohnani (umohnani at redhat dot com) diff --git a/docs/source/markdown/podman-kube-generate.1.md b/docs/source/markdown/podman-kube-generate.1.md index f46f86de6f..38f11409ad 100644 --- a/docs/source/markdown/podman-kube-generate.1.md +++ b/docs/source/markdown/podman-kube-generate.1.md @@ -36,8 +36,7 @@ Output to the given file, instead of STDOUT. If the file already exists, `kube g #### **--service**, **-s** -Generate a Kubernetes service object in addition to the Pods. Used to generate a Service specification for the corresponding Pod output. In particular, if the object has portmap bindings, the service specification will include a NodePort declaration to expose the service. A -random port is assigned by Podman in the specification. +Generate a Kubernetes service object in addition to the Pods. Used to generate a Service specification for the corresponding Pod output. In particular, if the object has portmap bindings, the service specification will include a NodePort declaration to expose the service. A random port is assigned by Podman in the specification. ## EXAMPLES diff --git a/docs/source/markdown/podman-kube.1.md b/docs/source/markdown/podman-kube.1.md index f70a61f477..76762df2dd 100644 --- a/docs/source/markdown/podman-kube.1.md +++ b/docs/source/markdown/podman-kube.1.md @@ -14,12 +14,13 @@ file input. Containers will be automatically started. | Command | Man Page | Description | | ------- | ---------------------------------------------------- | ----------------------------------------------------------------------------- | +| apply | [podman-kube-apply(1)](podman-kube-apply.1.md) | Apply Kubernetes YAML based on containers, pods, or volumes to a Kubernetes cluster | | down | [podman-kube-down(1)](podman-kube-down.1.md) | Remove containers and pods based on Kubernetes YAML. | | generate | [podman-kube-generate(1)](podman-kube-generate.1.md) | Generate Kubernetes YAML based on containers, pods or volumes. | | play | [podman-kube-play(1)](podman-kube-play.1.md) | Create containers, pods and volumes based on Kubernetes YAML. | ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-kube-down(1)](podman-kube-down.1.md)**, **[podman-kube-generate(1)](podman-kube-generate.1.md)** +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-kube-down(1)](podman-kube-down.1.md)**, **[podman-kube-generate(1)](podman-kube-generate.1.md)**, **[podman-kube-apply(1)](podman-kube-apply.1.md)** ## HISTORY December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/pkg/api/handlers/libpod/kube.go b/pkg/api/handlers/libpod/kube.go index 9e9ef52a50..e845aeaf58 100644 --- a/pkg/api/handlers/libpod/kube.go +++ b/pkg/api/handlers/libpod/kube.go @@ -126,3 +126,29 @@ func KubePlayDown(w http.ResponseWriter, r *http.Request) { func KubeGenerate(w http.ResponseWriter, r *http.Request) { GenerateKube(w, r) } + +func KubeApply(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) + query := struct { + CACertFile string `schema:"caCertFile"` + Kubeconfig string `schema:"kubeconfig"` + Namespace string `schema:"namespace"` + }{ + // Defaults would go here. + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) + return + } + + containerEngine := abi.ContainerEngine{Libpod: runtime} + options := entities.ApplyOptions{CACertFile: query.CACertFile, Kubeconfig: query.Kubeconfig, Namespace: query.Namespace} + if err := containerEngine.KubeApply(r.Context(), r.Body, options); err != nil { + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("error applying YAML to k8s cluster: %w", err)) + return + } + + utils.WriteResponse(w, http.StatusOK, "Deployed!") +} diff --git a/pkg/api/server/register_kube.go b/pkg/api/server/register_kube.go index 0c3cd1d040..850a1eba2a 100644 --- a/pkg/api/server/register_kube.go +++ b/pkg/api/server/register_kube.go @@ -111,5 +111,49 @@ func (s *APIServer) registerKubeHandlers(r *mux.Router) error { // $ref: "#/responses/internalError" r.HandleFunc(VersionedPath("/libpod/generate/kube"), s.APIHandler(libpod.GenerateKube)).Methods(http.MethodGet) r.HandleFunc(VersionedPath("/libpod/kube/generate"), s.APIHandler(libpod.KubeGenerate)).Methods(http.MethodGet) + // swagger:operation POST /libpod/kube/apply libpod KubeApplyLibpod + // --- + // tags: + // - containers + // - pods + // summary: Apply a podman workload or Kubernetes YAML file. + // description: Deploy a podman container, pod, volume, or Kubernetes yaml to a Kubernetes cluster. + // parameters: + // - in: query + // name: caCertFile + // type: string + // description: Path to the CA cert file for the Kubernetes cluster. + // - in: query + // name: kubeConfig + // type: string + // description: Path to the kubeconfig file for the Kubernetes cluster. + // - in: query + // name: namespace + // type: string + // description: The namespace to deploy the workload to on the Kubernetes cluster. + // - in: query + // name: service + // type: boolean + // description: Create a service object for the container being deployed. + // - in: query + // name: file + // type: string + // description: Path to the Kubernetes yaml file to deploy. + // - in: body + // name: request + // description: Kubernetes YAML file. + // schema: + // type: string + // produces: + // - application/json + // responses: + // 200: + // description: Kubernetes YAML file successfully deployed to cluster + // schema: + // type: string + // format: binary + // 500: + // $ref: "#/responses/internalError" + r.HandleFunc(VersionedPath("/libpod/kube/apply"), s.APIHandler(libpod.KubeApply)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/kube/kube.go b/pkg/bindings/kube/kube.go index 1b9f888ef7..91bab3f858 100644 --- a/pkg/bindings/kube/kube.go +++ b/pkg/bindings/kube/kube.go @@ -102,3 +102,41 @@ func DownWithBody(ctx context.Context, body io.Reader) (*entities.KubePlayReport func Generate(ctx context.Context, nameOrIDs []string, options generate.KubeOptions) (*entities.GenerateKubeReport, error) { return generate.Kube(ctx, nameOrIDs, &options) } + +func Apply(ctx context.Context, path string, options *ApplyOptions) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer func() { + if err := f.Close(); err != nil { + logrus.Warn(err) + } + }() + + return ApplyWithBody(ctx, f, options) +} + +func ApplyWithBody(ctx context.Context, body io.Reader, options *ApplyOptions) error { + if options == nil { + options = new(ApplyOptions) + } + + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + + params, err := options.ToParams() + if err != nil { + return err + } + + response, err := conn.DoRequest(ctx, body, http.MethodPost, "/kube/apply", params, nil) + if err != nil { + return err + } + defer response.Body.Close() + + return nil +} diff --git a/pkg/bindings/kube/types.go b/pkg/bindings/kube/types.go index 93057972e3..979eadebf3 100644 --- a/pkg/bindings/kube/types.go +++ b/pkg/bindings/kube/types.go @@ -47,3 +47,19 @@ type PlayOptions struct { // Userns - define the user namespace to use. Userns *string } + +// ApplyOptions are optional options for applying kube YAML files to a k8s cluster +// +//go:generate go run ../generator/generator.go ApplyOptions +type ApplyOptions struct { + // Kubeconfig - path to the cluster's kubeconfig file. + Kubeconfig *string + // Namespace - namespace to deploy the workload in on the cluster. + Namespace *string + // CACertFile - the path to the CA cert file for the Kubernetes cluster. + CACertFile *string + // File - the path to the Kubernetes yaml to deploy. + File *string + // Service - creates a service for the container being deployed. + Service *bool +} diff --git a/pkg/bindings/kube/types_apply_options.go b/pkg/bindings/kube/types_apply_options.go new file mode 100644 index 0000000000..9fb49594dd --- /dev/null +++ b/pkg/bindings/kube/types_apply_options.go @@ -0,0 +1,93 @@ +// Code generated by go generate; DO NOT EDIT. +package kube + +import ( + "net/url" + + "github.com/containers/podman/v4/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *ApplyOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *ApplyOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} + +// WithKubeconfig set field Kubeconfig to given value +func (o *ApplyOptions) WithKubeconfig(value string) *ApplyOptions { + o.Kubeconfig = &value + return o +} + +// GetKubeconfig returns value of field Kubeconfig +func (o *ApplyOptions) GetKubeconfig() string { + if o.Kubeconfig == nil { + var z string + return z + } + return *o.Kubeconfig +} + +// WithNamespace set field Namespace to given value +func (o *ApplyOptions) WithNamespace(value string) *ApplyOptions { + o.Namespace = &value + return o +} + +// GetNamespace returns value of field Namespace +func (o *ApplyOptions) GetNamespace() string { + if o.Namespace == nil { + var z string + return z + } + return *o.Namespace +} + +// WithCACertFile set field CACertFile to given value +func (o *ApplyOptions) WithCACertFile(value string) *ApplyOptions { + o.CACertFile = &value + return o +} + +// GetCACertFile returns value of field CACertFile +func (o *ApplyOptions) GetCACertFile() string { + if o.CACertFile == nil { + var z string + return z + } + return *o.CACertFile +} + +// WithFile set field File to given value +func (o *ApplyOptions) WithFile(value string) *ApplyOptions { + o.File = &value + return o +} + +// GetFile returns value of field File +func (o *ApplyOptions) GetFile() string { + if o.File == nil { + var z string + return z + } + return *o.File +} + +// WithService set field Service to given value +func (o *ApplyOptions) WithService(value bool) *ApplyOptions { + o.Service = &value + return o +} + +// GetService returns value of field Service +func (o *ApplyOptions) GetService() bool { + if o.Service == nil { + var z bool + return z + } + return *o.Service +} diff --git a/pkg/domain/entities/apply.go b/pkg/domain/entities/apply.go new file mode 100644 index 0000000000..e6d0775e49 --- /dev/null +++ b/pkg/domain/entities/apply.go @@ -0,0 +1,21 @@ +package entities + +var ( + TypePVC = "PersistentVolumeClaim" + TypePod = "Pod" + TypeService = "Service" +) + +// ApplyOptions controls the deployment of kube yaml files to a Kubernetes Cluster +type ApplyOptions struct { + // Kubeconfig - path to the cluster's kubeconfig file. + Kubeconfig string + // Namespace - namespace to deploy the workload in on the cluster. + Namespace string + // CACertFile - the path to the CA cert file for the Kubernetes cluster. + CACertFile string + // File - the path to the Kubernetes yaml to deploy. + File string + // Service - creates a service for the container being deployed. + Service bool +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 51058e318d..44c4f2e4b3 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -61,6 +61,7 @@ type ContainerEngine interface { //nolint:interfacebloat SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error) HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) + KubeApply(ctx context.Context, body io.Reader, opts ApplyOptions) error NetworkConnect(ctx context.Context, networkname string, options NetworkConnectOptions) error NetworkCreate(ctx context.Context, network types.Network) (*types.Network, error) NetworkDisconnect(ctx context.Context, networkname string, options NetworkDisconnectOptions) error diff --git a/pkg/domain/infra/abi/apply.go b/pkg/domain/infra/abi/apply.go new file mode 100644 index 0000000000..0e63d829d8 --- /dev/null +++ b/pkg/domain/infra/abi/apply.go @@ -0,0 +1,194 @@ +package abi + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/containers/podman/v4/pkg/domain/entities" + k8sAPI "github.com/containers/podman/v4/pkg/k8s.io/api/core/v1" + "github.com/ghodss/yaml" +) + +func (ic *ContainerEngine) KubeApply(ctx context.Context, body io.Reader, options entities.ApplyOptions) error { + // Read the yaml file + content, err := io.ReadAll(body) + if err != nil { + return err + } + if len(content) == 0 { + return errors.New("yaml file provided is empty, cannot apply to a cluster") + } + + // Split the yaml file + documentList, err := splitMultiDocYAML(content) + if err != nil { + return err + } + + // Sort the kube kinds + documentList, err = sortKubeKinds(documentList) + if err != nil { + return fmt.Errorf("unable to sort kube kinds: %w", err) + } + + // Get the namespace to deploy the workload to + namespace := options.Namespace + if namespace == "" { + namespace = "default" + } + + // Parse the given kubeconfig + kconfig, err := getClusterInfo(options.Kubeconfig) + if err != nil { + return err + } + + // Set up the client to connect to the cluster endpoints + client, err := setUpClusterClient(kconfig, options) + if err != nil { + return err + } + + for _, document := range documentList { + kind, err := getKubeKind(document) + if err != nil { + return fmt.Errorf("unable to read kube YAML: %w", err) + } + + switch kind { + case entities.TypeService: + url := kconfig.Clusters[0].Cluster.Server + "/api/v1/namespaces/" + namespace + "/services" + if err := createObject(client, url, document); err != nil { + return err + } + case entities.TypePVC: + url := kconfig.Clusters[0].Cluster.Server + "/api/v1/namespaces/" + namespace + "/persistentvolumeclaims" + if err := createObject(client, url, document); err != nil { + return err + } + case entities.TypePod: + url := kconfig.Clusters[0].Cluster.Server + "/api/v1/namespaces/" + namespace + "/pods" + if err := createObject(client, url, document); err != nil { + return err + } + default: + return fmt.Errorf("unsupported Kubernetes kind found: %q", kind) + } + } + + return nil +} + +// setUpClusterClient sets up the client to use when connecting to the cluster. It sets up the CA Certs and +// client certs and keys based on the information given in the kubeconfig +func setUpClusterClient(kconfig k8sAPI.Config, applyOptions entities.ApplyOptions) (*http.Client, error) { + var ( + clientCert tls.Certificate + err error + ) + + // Load client certificate and key + // This information will always be in the kubeconfig + if kconfig.AuthInfos[0].AuthInfo.ClientCertificate != "" && kconfig.AuthInfos[0].AuthInfo.ClientKey != "" { + clientCert, err = tls.LoadX509KeyPair(kconfig.AuthInfos[0].AuthInfo.ClientCertificate, kconfig.AuthInfos[0].AuthInfo.ClientKey) + if err != nil { + return nil, err + } + } else if len(kconfig.AuthInfos[0].AuthInfo.ClientCertificateData) > 0 && len(kconfig.AuthInfos[0].AuthInfo.ClientKeyData) > 0 { + clientCert, err = tls.X509KeyPair(kconfig.AuthInfos[0].AuthInfo.ClientCertificateData, kconfig.AuthInfos[0].AuthInfo.ClientKeyData) + if err != nil { + return nil, err + } + } + + // Load CA cert + // The CA cert may not always be in the kubeconfig and could be in a separate file. + // The CA cert file can be passed on here by setting the --ca-cert-file flag. If that is not set + // check the kubeconfig to see if it has the CA cert data. + var caCert []byte + insecureSkipVerify := false + caCertFile := applyOptions.CACertFile + caCertPool := x509.NewCertPool() + + // Be insecure if user sets ca-cert-file flag to insecure + if strings.ToLower(caCertFile) == "insecure" { + insecureSkipVerify = true + } else if caCertFile == "" { + caCertFile = kconfig.Clusters[0].Cluster.CertificateAuthority + } + + // Get the caCert data if we are running secure + if caCertFile != "" && !insecureSkipVerify { + caCert, err = os.ReadFile(caCertFile) + if err != nil { + return nil, err + } + } else if len(kconfig.Clusters[0].Cluster.CertificateAuthorityData) > 0 && !insecureSkipVerify { + caCert = kconfig.Clusters[0].Cluster.CertificateAuthorityData + } + if len(caCert) > 0 { + caCertPool.AppendCertsFromPEM(caCert) + } + + // Create transport with ca and client certs + tr := &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: caCertPool, Certificates: []tls.Certificate{clientCert}, InsecureSkipVerify: insecureSkipVerify}, + } + return &http.Client{Transport: tr}, nil +} + +// createObject connects to the given url and creates the yaml given in objectData +func createObject(client *http.Client, url string, objectData []byte) error { + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(string(objectData))) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/yaml") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Log the response body as fatal if we get a non-success status code + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted { + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return errors.New(string(body)) + } + return nil +} + +// getClusterInfo returns the kubeconfig in struct form so that the server +// and certificates data can be accessed and used to connect to the k8s cluster +func getClusterInfo(kubeconfig string) (k8sAPI.Config, error) { + var config k8sAPI.Config + + configData, err := os.ReadFile(kubeconfig) + if err != nil { + return config, err + } + + // Convert yaml kubeconfig to json so we can unmarshal it + jsonData, err := yaml.YAMLToJSON(configData) + if err != nil { + return config, err + } + + if err := json.Unmarshal(jsonData, &config); err != nil { + return config, err + } + + return config, nil +} diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/kube.go similarity index 50% rename from pkg/domain/infra/tunnel/generate.go rename to pkg/domain/infra/tunnel/kube.go index d3c3638cb3..22be7eea73 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/kube.go @@ -3,8 +3,12 @@ package tunnel import ( "context" "fmt" + "io" + "github.com/containers/image/v5/types" "github.com/containers/podman/v4/pkg/bindings/generate" + "github.com/containers/podman/v4/pkg/bindings/kube" + "github.com/containers/podman/v4/pkg/bindings/play" "github.com/containers/podman/v4/pkg/domain/entities" ) @@ -49,3 +53,33 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, func (ic *ContainerEngine) GenerateSpec(ctx context.Context, opts *entities.GenerateSpecOptions) (*entities.GenerateSpecReport, error) { return nil, fmt.Errorf("GenerateSpec is not supported on the remote API") } + +func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, opts entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + options := new(kube.PlayOptions).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.Networks).WithSeccompProfileRoot(opts.SeccompProfileRoot) + options.WithStaticIPs(opts.StaticIPs).WithStaticMACs(opts.StaticMACs) + if len(opts.LogOptions) > 0 { + options.WithLogOptions(opts.LogOptions) + } + if opts.Annotations != nil { + options.WithAnnotations(opts.Annotations) + } + options.WithNoHosts(opts.NoHosts).WithUserns(opts.Userns) + if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined { + options.WithSkipTLSVerify(s == types.OptionalBoolTrue) + } + if start := opts.Start; start != types.OptionalBoolUndefined { + options.WithStart(start == types.OptionalBoolTrue) + } + return play.KubeWithBody(ic.ClientCtx, body, options) +} + +func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, body io.Reader, _ entities.PlayKubeDownOptions) (*entities.PlayKubeReport, error) { + return play.DownWithBody(ic.ClientCtx, body) +} + +func (ic *ContainerEngine) KubeApply(ctx context.Context, body io.Reader, opts entities.ApplyOptions) error { + options := new(kube.ApplyOptions).WithKubeconfig(opts.Kubeconfig).WithCACertFile(opts.CACertFile).WithNamespace(opts.Namespace) + return kube.ApplyWithBody(ic.ClientCtx, body, options) +} diff --git a/pkg/domain/infra/tunnel/play.go b/pkg/domain/infra/tunnel/play.go deleted file mode 100644 index ee9195681a..0000000000 --- a/pkg/domain/infra/tunnel/play.go +++ /dev/null @@ -1,36 +0,0 @@ -package tunnel - -import ( - "context" - "io" - - "github.com/containers/image/v5/types" - "github.com/containers/podman/v4/pkg/bindings/kube" - "github.com/containers/podman/v4/pkg/bindings/play" - "github.com/containers/podman/v4/pkg/domain/entities" -) - -func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, opts entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { - options := new(kube.PlayOptions).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.Networks).WithSeccompProfileRoot(opts.SeccompProfileRoot) - options.WithStaticIPs(opts.StaticIPs).WithStaticMACs(opts.StaticMACs) - if len(opts.LogOptions) > 0 { - options.WithLogOptions(opts.LogOptions) - } - if opts.Annotations != nil { - options.WithAnnotations(opts.Annotations) - } - options.WithNoHosts(opts.NoHosts).WithUserns(opts.Userns) - if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined { - options.WithSkipTLSVerify(s == types.OptionalBoolTrue) - } - if start := opts.Start; start != types.OptionalBoolUndefined { - options.WithStart(start == types.OptionalBoolTrue) - } - return play.KubeWithBody(ic.ClientCtx, body, options) -} - -func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, body io.Reader, _ entities.PlayKubeDownOptions) (*entities.PlayKubeReport, error) { - return play.DownWithBody(ic.ClientCtx, body) -} diff --git a/pkg/k8s.io/api/core/v1/types.go b/pkg/k8s.io/api/core/v1/types.go index 114c552e5a..47b2689786 100644 --- a/pkg/k8s.io/api/core/v1/types.go +++ b/pkg/k8s.io/api/core/v1/types.go @@ -4490,3 +4490,248 @@ type PortStatus struct { // +kubebuilder:validation:MaxLength=316 Error *string `json:"error,omitempty"` } + +// The following has been copied from https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/api/v1/types.go +// It holds the struct information for a kubeconfig that let's us unmarshal a given kubeconfig so that we can deploy workloads +// to the cluster with podman generate kube. + +// Config holds the information needed to build connect to remote kubernetes clusters as a given user +type Config struct { + // Legacy field from pkg/api/types.go TypeMeta. + // TODO(jlowdermilk): remove this after eliminating downstream dependencies. + // +k8s:conversion-gen=false + // +optional + Kind string `json:"kind,omitempty"` + // Legacy field from pkg/api/types.go TypeMeta. + // TODO(jlowdermilk): remove this after eliminating downstream dependencies. + // +k8s:conversion-gen=false + // +optional + APIVersion string `json:"apiVersion,omitempty"` + // Preferences holds general information to be use for cli interactions + Preferences Preferences `json:"preferences"` + // Clusters is a map of referencable names to cluster configs + Clusters []NamedCluster `json:"clusters"` + // AuthInfos is a map of referencable names to user configs + AuthInfos []NamedAuthInfo `json:"users"` + // Contexts is a map of referencable names to context configs + Contexts []NamedContext `json:"contexts"` + // CurrentContext is the name of the context that you would like to use by default + CurrentContext string `json:"current-context"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + // +optional + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +type Preferences struct { + // +optional + Colors bool `json:"colors,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + // +optional + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +// Cluster contains information about how to communicate with a kubernetes cluster +type Cluster struct { + // Server is the address of the kubernetes cluster (https://hostname:port). + Server string `json:"server"` + // TLSServerName is used to check server certificate. If TLSServerName is empty, the hostname used to contact the server is used. + // +optional + TLSServerName string `json:"tls-server-name,omitempty"` + // InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure. + // +optional + InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` + // CertificateAuthority is the path to a cert file for the certificate authority. + // +optional + CertificateAuthority string `json:"certificate-authority,omitempty"` + // CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority + // +optional + CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` + // ProxyURL is the URL to the proxy to be used for all requests made by this + // client. URLs with "http", "https", and "socks5" schemes are supported. If + // this configuration is not provided or the empty string, the client + // attempts to construct a proxy configuration from http_proxy and + // https_proxy environment variables. If these environment variables are not + // set, the client does not attempt to proxy requests. + // + // socks5 proxying does not currently support spdy streaming endpoints (exec, + // attach, port forward). + // +optional + ProxyURL string `json:"proxy-url,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + // +optional + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are. +type AuthInfo struct { + // ClientCertificate is the path to a client cert file for TLS. + // +optional + ClientCertificate string `json:"client-certificate,omitempty"` + // ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate + // +optional + ClientCertificateData []byte `json:"client-certificate-data,omitempty"` + // ClientKey is the path to a client key file for TLS. + // +optional + ClientKey string `json:"client-key,omitempty"` + // ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey + // +optional + ClientKeyData []byte `json:"client-key-data,omitempty" datapolicy:"security-key"` + // Token is the bearer token for authentication to the kubernetes cluster. + // +optional + Token string `json:"token,omitempty" datapolicy:"token"` + // TokenFile is a pointer to a file that contains a bearer token (as described above). If both Token and TokenFile are present, Token takes precedence. + // +optional + TokenFile string `json:"tokenFile,omitempty"` + // Impersonate is the username to impersonate. The name matches the flag. + // +optional + Impersonate string `json:"as,omitempty"` + // ImpersonateUID is the uid to impersonate. + // +optional + ImpersonateUID string `json:"as-uid,omitempty"` + // ImpersonateGroups is the groups to impersonate. + // +optional + ImpersonateGroups []string `json:"as-groups,omitempty"` + // ImpersonateUserExtra contains additional information for impersonated user. + // +optional + ImpersonateUserExtra map[string][]string `json:"as-user-extra,omitempty"` + // Username is the username for basic authentication to the kubernetes cluster. + // +optional + Username string `json:"username,omitempty"` + // Password is the password for basic authentication to the kubernetes cluster. + // +optional + Password string `json:"password,omitempty" datapolicy:"password"` + // AuthProvider specifies a custom authentication plugin for the kubernetes cluster. + // +optional + AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"` + // Exec specifies a custom exec-based authentication plugin for the kubernetes cluster. + // +optional + Exec *ExecConfig `json:"exec,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + // +optional + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) +type Context struct { + // Cluster is the name of the cluster for this context + Cluster string `json:"cluster"` + // AuthInfo is the name of the authInfo for this context + AuthInfo string `json:"user"` + // Namespace is the default namespace to use on unspecified requests + // +optional + Namespace string `json:"namespace,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + // +optional + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +// NamedCluster relates nicknames to cluster information +type NamedCluster struct { + // Name is the nickname for this Cluster + Name string `json:"name"` + // Cluster holds the cluster information + Cluster Cluster `json:"cluster"` +} + +// NamedContext relates nicknames to context information +type NamedContext struct { + // Name is the nickname for this Context + Name string `json:"name"` + // Context holds the context information + Context Context `json:"context"` +} + +// NamedAuthInfo relates nicknames to auth information +type NamedAuthInfo struct { + // Name is the nickname for this AuthInfo + Name string `json:"name"` + // AuthInfo holds the auth information + AuthInfo AuthInfo `json:"user"` +} + +// NamedExtension relates nicknames to extension information +type NamedExtension struct { + // Name is the nickname for this Extension + Name string `json:"name"` + // Extension holds the extension information + Extension interface{} `json:"extension"` +} + +// AuthProviderConfig holds the configuration for a specified auth provider. +type AuthProviderConfig struct { + Name string `json:"name"` + Config map[string]string `json:"config"` +} + +// ExecConfig specifies a command to provide client credentials. The command is exec'd +// and outputs structured stdout holding credentials. +// +// See the client.authentication.k8s.io API group for specifications of the exact input +// and output format +type ExecConfig struct { + // Command to execute. + Command string `json:"command"` + // Arguments to pass to the command when executing it. + // +optional + Args []string `json:"args"` + // Env defines additional environment variables to expose to the process. These + // are unioned with the host's environment, as well as variables client-go uses + // to pass argument to the plugin. + // +optional + Env []ExecEnvVar `json:"env"` + + // Preferred input version of the ExecInfo. The returned ExecCredentials MUST use + // the same encoding version as the input. + APIVersion string `json:"apiVersion,omitempty"` + + // This text is shown to the user when the executable doesn't seem to be + // present. For example, `brew install foo-cli` might be a good InstallHint for + // foo-cli on Mac OS systems. + InstallHint string `json:"installHint,omitempty"` + + // ProvideClusterInfo determines whether or not to provide cluster information, + // which could potentially contain very large CA data, to this exec plugin as a + // part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set + // to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for + // reading this environment variable. + ProvideClusterInfo bool `json:"provideClusterInfo"` + + // InteractiveMode determines this plugin's relationship with standard input. Valid + // values are "Never" (this exec plugin never uses standard input), "IfAvailable" (this + // exec plugin wants to use standard input if it is available), or "Always" (this exec + // plugin requires standard input to function). See ExecInteractiveMode values for more + // details. + // + // If APIVersion is client.authentication.k8s.io/v1alpha1 or + // client.authentication.k8s.io/v1beta1, then this field is optional and defaults + // to "IfAvailable" when unset. Otherwise, this field is required. + //+optional + InteractiveMode ExecInteractiveMode `json:"interactiveMode,omitempty"` +} + +// ExecEnvVar is used for setting environment variables when executing an exec-based +// credential plugin. +type ExecEnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// ExecInteractiveMode is a string that describes an exec plugin's relationship with standard input. +type ExecInteractiveMode string + +const ( + // NeverExecInteractiveMode declares that this exec plugin never needs to use standard + // input, and therefore the exec plugin will be run regardless of whether standard input is + // available for user input. + NeverExecInteractiveMode ExecInteractiveMode = "Never" + // IfAvailableExecInteractiveMode declares that this exec plugin would like to use standard input + // if it is available, but can still operate if standard input is not available. Therefore, the + // exec plugin will be run regardless of whether stdin is available for user input. If standard + // input is available for user input, then it will be provided to this exec plugin. + IfAvailableExecInteractiveMode ExecInteractiveMode = "IfAvailable" + // AlwaysExecInteractiveMode declares that this exec plugin requires standard input in order to + // run, and therefore the exec plugin will only be run if standard input is available for user + // input. If standard input is not available for user input, then the exec plugin will not be run + // and an error will be returned by the exec plugin runner. + AlwaysExecInteractiveMode ExecInteractiveMode = "Always" +) diff --git a/test/minikube/001-kube.bats b/test/minikube/001-kube.bats index 4dd8df12ab..c8e037f644 100755 --- a/test/minikube/001-kube.bats +++ b/test/minikube/001-kube.bats @@ -15,7 +15,6 @@ load helpers.bash run minikube kubectl get pods assert "$status" -eq 0 "get pods in the default namespace" assert "$output" == "No resources found in default namespace." - wait_for_default_sa } @test "minikube - deploy generated container yaml to minikube" { @@ -29,7 +28,6 @@ load helpers.bash run minikube kubectl create namespace $project assert "$status" -eq 0 "create new namespace $project" run minikube kubectl -- apply -f $fname - echo $output >&2 assert "$status" -eq 0 "deploy $fname to the cluster" assert "$output" == "pod/$cname-pod created" wait_for_pods_to_start @@ -59,3 +57,108 @@ load helpers.bash run minikube kubectl delete namespace $project assert $status -eq 0 "delete namespace $project" } + +@test "minikube - apply podman ctr to cluster" { + cname="test-ctr-apply" + run_podman container create --name $cname $IMAGE top + + # deploy to minikube cluster with kube apply + project="ctr-apply" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run_podman kube apply --kubeconfig $KUBECONFIG --ns $project $cname + assert "$output" =~ "Successfully deployed workloads to cluster!" + run minikube kubectl -- get pods --namespace $project + assert "$status" -eq 0 "kube apply $cname to the cluster" + assert "$output" =~ "$cname-pod" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} + +@test "minikube - apply podman pod to cluster" { + pname="test-pod-apply" + run_podman pod create --name $pname + run podman container create --pod $pname $IMAGE top + + # deploy to minikube cluster with kube apply + project="pod-apply" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run_podman kube apply --kubeconfig $KUBECONFIG --ns $project $pname + assert "$output" =~ "Successfully deployed workloads to cluster!" + run minikube kubectl -- get pods --namespace $project + assert "$status" -eq 0 "kube apply $pname to the cluster" + assert "$output" =~ "$pname" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} + +@test "minikube - deploy generated kube yaml with podman kube apply to cluster" { + pname="test-pod" + cname1="test-ctr1" + cname2="test-ctr2" + fname="/tmp/minikube_deploy_$(random_string 6).yaml" + + run_podman pod create --name $pname --publish 9999:8888 + run_podman container create --name $cname1 --pod $pname $IMAGE sleep 1000 + run_podman container create --name $cname2 --pod $pname $IMAGE sleep 2000 + run_podman kube generate -f $fname $pname + + # deploy to minikube cluster with kube apply + project="yaml-apply" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run_podman kube apply --kubeconfig $KUBECONFIG --ns $project -f $fname + assert "$output" =~ "Successfully deployed workloads to cluster!" + run minikube kubectl -- get pods --namespace $project + assert "$status" -eq 0 "kube apply $pname to the cluster" + assert "$output" =~ "$pname" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} + +@test "minikube - apply podman ctr with volume to cluster" { + cname="ctr-vol" + vname="myvol" + run_podman container create -v $vname:/myvol --name $cname $IMAGE top + + # deploy to minikube cluster with kube apply + project="ctr-vol-apply" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run_podman kube apply --kubeconfig $KUBECONFIG --ns $project $cname $vname + assert "$output" =~ "Successfully deployed workloads to cluster!" + run minikube kubectl -- get pods --namespace $project + assert "$status" -eq 0 "kube apply $cname to the cluster" + assert "$output" =~ "$cname-pod" + run minikube kubectl -- get pvc --namespace $project + assert "$status" -eq 0 "kube apply $vname to the cluster" + assert "$output" =~ "$vname" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} + +@test "minikube - apply podman ctr with service to cluster" { + cname="ctr-svc" + run_podman container create -p 3000:4000 --name $cname $IMAGE top + + # deploy to minikube cluster with kube apply + project="ctr-svc-apply" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run_podman kube apply --kubeconfig $KUBECONFIG -s --ns $project $cname + assert "$output" =~ "Successfully deployed workloads to cluster!" + run minikube kubectl -- get pods --namespace $project + assert "$status" -eq 0 "kube apply $cname to the cluster" + assert "$output" =~ "$cname-pod" + run minikube kubectl -- get svc --namespace $project + assert "$status" -eq 0 "kube apply service to the cluster" + assert "$output" =~ "$cname-pod" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} diff --git a/test/minikube/helpers.bash b/test/minikube/helpers.bash index a89594622e..c47b711e3d 100644 --- a/test/minikube/helpers.bash +++ b/test/minikube/helpers.bash @@ -2,10 +2,13 @@ load ../system/helpers.bash +KUBECONFIG="$HOME/.kube/config" + function setup(){ # only set up the minikube cluster before the first test if [[ "$BATS_TEST_NUMBER" -eq 1 ]]; then minikube start + wait_for_default_sa fi basic_setup }