Add podman kube apply command

Add the abilitiy to deploy the generated kube yaml to a
kubernetes cluster with the podman kube apply command.
Add support to directly apply containers, pods, or volumes
by passing in their names or ids to the command.
Use the kubernetes API endpoints and http requests to connect
to the cluster and deploy the various kubernetes object kinds.

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>
This commit is contained in:
Urvashi Mohnani
2022-07-27 13:24:59 -04:00
parent fa19f57ebe
commit f6c74324bc
17 changed files with 1007 additions and 41 deletions

111
cmd/podman/kube/apply.go Normal file
View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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