package kubernetes import ( "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "os" "strings" portainer "github.com/portainer/portainer/api" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) // KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data type KubeClusterAccessService interface { IsSecure() bool GetData(hostURL string, endpointId portainer.EndpointID) kubernetesClusterAccessData } // KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data type kubernetesClusterAccessData struct { ClusterServerURL string `example:"https://mycompany.k8s.com"` CertificateAuthorityFile string `example:"/data/tls/localhost.crt"` CertificateAuthorityData string `example:"MIIC5TCCAc2gAwIBAgIJAJ+...+xuhOaFXwQ=="` } type kubeClusterAccessService struct { baseURL string httpsBindAddr string certificateAuthorityFile string certificateAuthorityData string } var ( errTLSCertNotProvided = errors.New("tls cert path not provided") errTLSCertFileMissing = errors.New("missing tls cert file") errTLSCertIncorrectType = errors.New("incorrect tls cert type") errTLSCertValidation = errors.New("failed to parse tls certificate") ) // NewKubeClusterAccessService creates a new instance of a KubeClusterAccessService func NewKubeClusterAccessService(baseURL, httpsBindAddr, tlsCertPath string) KubeClusterAccessService { certificateAuthorityData, err := getCertificateAuthorityData(tlsCertPath) if err != nil { log.Debug().Err(err).Msg("generated KubeConfig will be insecure") } return &kubeClusterAccessService{ baseURL: baseURL, httpsBindAddr: httpsBindAddr, certificateAuthorityFile: tlsCertPath, certificateAuthorityData: certificateAuthorityData, } } // getCertificateAuthorityData reads tls certificate from supplied path and verifies the tls certificate // then returns content (string) of the certificate within `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` func getCertificateAuthorityData(tlsCertPath string) (string, error) { if tlsCertPath == "" { return "", errTLSCertNotProvided } data, err := os.ReadFile(tlsCertPath) if err != nil { return "", errors.Wrap(errTLSCertFileMissing, err.Error()) } block, _ := pem.Decode(data) if block == nil || block.Type != "CERTIFICATE" { return "", errTLSCertIncorrectType } certificate, err := x509.ParseCertificate(block.Bytes) if err != nil { return "", errors.Wrap(errTLSCertValidation, err.Error()) } return base64.StdEncoding.EncodeToString(certificate.Raw), nil } // IsSecure specifies whether generated KubeConfig structs from the service will not have `insecure-skip-tls-verify: true` // this is based on the fact that we can successfully extract `certificateAuthorityData` from // certificate file at `tlsCertPath`. If we can successfully extract `certificateAuthorityData`, // then this will be used as `certificate-authority-data` attribute in a generated KubeConfig. func (service *kubeClusterAccessService) IsSecure() bool { return service.certificateAuthorityData != "" } // GetData returns K8s cluster access details for the specified environment(endpoint). // The struct can be used to: // - generate a kubeconfig file // - pass down params to binaries func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData { baseURL := service.baseURL // When the api call is internal, the baseURL should not be used. if hostURL == "localhost" { hostURL = hostURL + service.httpsBindAddr baseURL = "/" } if baseURL != "/" { baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/")) } log.Debug(). Str("host_URL", hostURL). Str("HTTPS_bind_address", service.httpsBindAddr). Str("base_URL", baseURL). Msg("kubeconfig") clusterURL := hostURL + baseURL clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID) return kubernetesClusterAccessData{ ClusterServerURL: clusterServerURL, CertificateAuthorityFile: service.certificateAuthorityFile, CertificateAuthorityData: service.certificateAuthorityData, } }