podman-build/pkg/domain/infra/abi/apply.go
2025-10-11 12:30:35 +09:00

197 lines
5.7 KiB
Go

//go:build !remote
package abi
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/containers/podman/v5/pkg/domain/entities"
k8sAPI "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1"
"sigs.k8s.io/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
}