197 lines
5.7 KiB
Go
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
|
|
}
|