mirror of
https://github.com/containers/podman.git
synced 2025-10-25 02:04:43 +08:00
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:
21
pkg/domain/entities/apply.go
Normal file
21
pkg/domain/entities/apply.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
194
pkg/domain/infra/abi/apply.go
Normal file
194
pkg/domain/infra/abi/apply.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user