Merge pull request from umohnani8/lift

Add podman kube apply command
This commit is contained in:
OpenShift Merge Robot
2022-11-02 14:38:13 -04:00
committed by GitHub
17 changed files with 1007 additions and 41 deletions

111
cmd/podman/kube/apply.go Normal file

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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