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

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