cmd/hub/kube/kubernetes.go (422 lines of code) (raw):
// Copyright (c) 2022 EPAM Systems, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kube
import (
"encoding/base64"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/manifest"
"github.com/epam/hubctl/cmd/hub/parameters"
"github.com/epam/hubctl/cmd/hub/util"
)
const (
stackNameOutput = "dns.name"
stackDomainOutput = "dns.domain"
kubernetesFlavorOutput = "kubernetes.flavor"
kubernetesApiEndpointOutput = "kubernetes.api.endpoint"
kubernetesApiTokenOutput = "kubernetes.api.token"
kubernetesApiCaCertOutput = "kubernetes.api.caCert"
kubernetesApiClientCertOutput = "kubernetes.api.clientCert"
kubernetesApiClientKeyOutput = "kubernetes.api.clientKey"
kubernetesEksClusterOutput = "kubernetes.eks.cluster"
kubernetesGkeClusterOutput = "kubernetes.gke.cluster"
kubernetesFileRefPrefix = "file:"
)
var (
KubernetesDefaultProviders = []string{
"kubernetes", "eks", "gke", "aks",
"stack-k8s-aws", "stack-k8s-eks", "stack-k8s-gke", "stack-k8s-aks",
"k8s-aws", "k8s-eks", "k8s-gke", "k8s-aks", "k8s-hybrid",
"k8s-metal", "k8s-openshift"}
kubernetesApiKeysFileSuf = map[string]string{
kubernetesApiCaCertOutput: "-ca.pem",
kubernetesApiClientCertOutput: "-client.pem",
kubernetesApiClientKeyOutput: "-client-key.pem",
}
KubernetesParameters = []string{stackDomainOutput, kubernetesFlavorOutput, kubernetesApiEndpointOutput,
kubernetesApiTokenOutput, kubernetesApiCaCertOutput, kubernetesApiClientCertOutput, kubernetesApiClientKeyOutput,
kubernetesEksClusterOutput, kubernetesGkeClusterOutput}
KubernetesKeysParameters = []string{
kubernetesApiCaCertOutput, kubernetesApiClientCertOutput, kubernetesApiClientKeyOutput}
KubernetesSecretParameters = []string{
kubernetesApiTokenOutput, kubernetesApiCaCertOutput, kubernetesApiClientCertOutput, kubernetesApiClientKeyOutput}
)
func CaptureKubernetes(component *manifest.ComponentRef, stackBaseDir string, componentsBaseDir string,
componentOutputs parameters.CapturedOutputs) parameters.CapturedOutputs {
outputs := make(parameters.CapturedOutputs)
componentName := manifest.ComponentQualifiedNameFromRef(component)
flavor := "k8s-aws"
if o, exist := componentOutputs[parameters.OutputQualifiedName(kubernetesFlavorOutput, componentName)]; exist {
flavor = util.String(o.Value)
}
domainQName := parameters.OutputQualifiedName(stackDomainOutput, componentName)
if _, exists := componentOutputs[domainQName]; !exists {
util.Warn("Component `%s` declared to provide Kubernetes but no `%s` output found", componentName, domainQName)
if len(componentOutputs) > 0 {
log.Print("Outputs:")
parameters.PrintCapturedOutputs(componentOutputs)
}
return outputs
}
switch flavor {
case "k8s-aws":
var missing []string
for _, outputName := range KubernetesKeysParameters {
outputQName := parameters.OutputQualifiedName(outputName, componentName)
if _, exists := componentOutputs[outputQName]; !exists {
missing = append(missing, outputName)
}
}
if len(missing) > 0 {
util.Warn("Component `%s` declared to provide Kubernetes but some key/certs output(s) are missing: %v",
componentName, missing)
}
case "eks":
caQName := parameters.OutputQualifiedName(kubernetesApiCaCertOutput, componentName)
_, exists := componentOutputs[caQName]
if !exists {
util.Warn("Component `%s` declared to provide EKS Kubernetes but no `%s` output found", componentName, caQName)
if config.Debug && len(componentOutputs) > 0 {
log.Print("Outputs:")
parameters.PrintCapturedOutputs(componentOutputs)
}
}
case "openshift":
tokenQName := parameters.OutputQualifiedName(kubernetesApiTokenOutput, componentName)
_, exists := componentOutputs[tokenQName]
if !exists {
util.Warn("Component `%s` declared to provide OpenShift Kubernetes but no `%s` output found", componentName, tokenQName)
if config.Debug && len(componentOutputs) > 0 {
log.Print("Outputs:")
parameters.PrintCapturedOutputs(componentOutputs)
}
}
case "gke":
caQName := parameters.OutputQualifiedName(kubernetesApiCaCertOutput, componentName)
tokenQName := parameters.OutputQualifiedName(kubernetesApiTokenOutput, componentName)
_, caExists := componentOutputs[caQName]
_, tokenExists := componentOutputs[tokenQName]
if !caExists || !tokenExists {
util.Warn("Component `%s` declared to provide GKE Kubernetes but no `%s` or `%s` output found", componentName, tokenQName, caQName)
if config.Debug && len(componentOutputs) > 0 {
log.Print("Outputs:")
parameters.PrintCapturedOutputs(componentOutputs)
}
}
case "aks":
caQName := parameters.OutputQualifiedName(kubernetesApiCaCertOutput, componentName)
tokenQName := parameters.OutputQualifiedName(kubernetesApiTokenOutput, componentName)
_, caExists := componentOutputs[caQName]
_, tokenExists := componentOutputs[tokenQName]
if !caExists || !tokenExists {
util.Warn("Component `%s` declared to provide AKS Kubernetes but no `%s` or `%s` output found", componentName, tokenQName, caQName)
if config.Debug && len(componentOutputs) > 0 {
log.Print("Outputs:")
parameters.PrintCapturedOutputs(componentOutputs)
}
}
}
return outputs
}
// Returns the first non-empty environment variable value and name
func getFirstEnviron(name ...string) (string, string) {
for _, n := range name {
if v := os.Getenv(n); v != "" {
return n, v
}
}
return "", ""
}
func SetupKubernetes(params parameters.LockedParameters,
provider string, outputs parameters.CapturedOutputs,
context string, overwrite, keepPems bool) {
kubectl := "kubectl"
domain, _ := mayOutput(params, outputs, provider, stackDomainOutput)
if domain == "" {
util.Debug("Parameters from %s are not providing: %s", provider, stackDomainOutput) // try to get domain from environment
name, val := getFirstEnviron("HUB_DOMAIN_NAME", "DOMAIN_NAME")
if val != "" {
util.Debug("Using %s from %s variable as %s", val, name, stackDomainOutput)
domain = val
} else {
// Porting fuzzy logic here from extensions, for compatibility
// When stack doesn't have a ingress domain.
// In this case user will declare only a stack name
util.Debug("Cannot find dns.domain from variables")
util.Debug("Trying %s instead for stack that doesn't use ingress", stackNameOutput)
stackName, _ := mayOutput(params, outputs, provider, stackNameOutput)
if stackName != "" {
domain = stackName
} else {
util.Debug("Trying environment variables")
name, val = getFirstEnviron("HUB_STACK_NAME", "STACK_NAME")
if val != "" {
util.Debug("Using %s from %s variable as %s", val, name, stackNameOutput)
domain = val
} else {
util.Warn("Giving up with domain name from %s", provider)
}
}
}
}
if domain == "" {
util.Errors("Unable to setup Kubeconfig: no domain name found")
os.Exit(1)
return
}
if context != "" {
util.Debug("Using kube context: %s", context)
}
if context == "" {
context = domain
}
flavor, _ := mayOutput(params, outputs, provider, kubernetesFlavorOutput)
if flavor == "" {
flavor = "k8s-aws"
}
eksClusterName := ""
bearerToken := ""
switch flavor {
case "eks":
eksClusterName = mustOutput(params, outputs, provider, kubernetesEksClusterOutput)
bearerToken, _ = mayOutput(params, outputs, provider, kubernetesApiTokenOutput)
case "openshift", "gke", "aks":
bearerToken = mustOutput(params, outputs, provider, kubernetesApiTokenOutput)
}
configFilename, err := kubeconfigFilename()
if err != nil {
util.WarnOnce("Unable to setup Kubeconfig: %v", err)
return
}
var configCmd string
if config.SwitchKubeconfigContext {
if config.Verbose {
log.Printf("Changing Kubeconfig context to `%s`", context)
}
configCmd = "use-context"
} else {
if config.Verbose {
log.Printf("Checking Kubeconfig context `%s`", context)
}
configCmd = "get-contexts"
}
outBytes, err := execOutput(kubectl, "config", configCmd, context)
if err == nil {
if !overwrite {
// check CA cert match to
// catch Kubeconfig leftovers from previous deployment to the same domain name
if ca, exist := mayOutput(params, outputs, provider, kubernetesApiCaCertOutput); exist && ca != "" {
warnClusterCaCertMismatch(configFilename, context, ca)
}
return
}
} else {
out := string(outBytes)
if !strings.Contains(out, "no context exists") && !strings.Contains(out, "not found") {
util.MaybeFatalf("kubectl failed: %v", err)
}
}
if config.Verbose {
if provider != "" {
log.Printf("Setting up Kubeconfig from `%s` outputs", provider)
if config.Debug {
parameters.PrintCapturedOutputsByComponent(outputs, provider)
}
} else {
log.Printf("Setting up Kubeconfig from stack parameters")
}
}
filenameBase := filepath.Join(filepath.Dir(configFilename), strings.Replace(domain, ".", "-", -1))
caCertFile := filenameBase + kubernetesApiKeysFileSuf[kubernetesApiCaCertOutput]
clientCertFile := filenameBase + kubernetesApiKeysFileSuf[kubernetesApiClientCertOutput]
clientKeyFile := filenameBase + kubernetesApiKeysFileSuf[kubernetesApiClientKeyOutput]
var caCert string
caCertExist := true
if flavor != "openshift" {
caCert = mustOutput(params, outputs, provider, kubernetesApiCaCertOutput)
} else {
caCert, caCertExist = mayOutput(params, outputs, provider, kubernetesApiCaCertOutput)
}
var pemsWritten []string
if caCertExist {
writeFile(caCertFile, caCert)
pemsWritten = append(pemsWritten, caCertFile)
}
if util.Contains([]string{"k8s-aws", "hybrid", "metal"}, flavor) {
writeFile(clientCertFile,
mustOutput(params, outputs, provider, kubernetesApiClientCertOutput))
writeFile(clientKeyFile,
mustOutput(params, outputs, provider, kubernetesApiClientKeyOutput))
pemsWritten = append(pemsWritten, clientCertFile, clientKeyFile)
}
apiEndpoint := domain
if endpoint, exist := mayOutput(params, outputs, provider, kubernetesApiEndpointOutput); exist && endpoint != "" {
apiEndpoint = endpoint
}
clusterArgs := []string{"config", "set-cluster", domain, "--server=https://" + apiEndpoint}
if caCertExist {
clusterArgs = append(clusterArgs, "--embed-certs=true", "--certificate-authority="+caCertFile)
}
mustExec(kubectl, clusterArgs...)
user := ""
switch flavor {
case "k8s-aws", "hybrid", "metal":
user = "admin@" + domain
mustExec(kubectl, "config", "set-credentials", user,
"--embed-certs=true",
"--client-key="+clientKeyFile,
"--client-certificate="+clientCertFile)
case "eks":
user = "eks-" + eksClusterName
if bearerToken != "" {
mustExec(kubectl, "config", "set-credentials", user,
"--token="+bearerToken)
mustExec(kubectl, "config", "unset", fmt.Sprintf("users.%s.exec", user))
} else {
mustExec(kubectl, "config", "set-credentials", user,
"--exec-api-version=client.authentication.k8s.io/v1alpha1",
"--exec-command=aws-iam-authenticator",
"--exec-arg=token",
"--exec-arg=-i",
"--exec-arg="+eksClusterName)
mustExec(kubectl, "config", "unset", fmt.Sprintf("users.%s.token", user))
}
case "openshift", "gke", "aks":
user = fmt.Sprintf("%s-%s", flavor, domain)
mustExec(kubectl, "config", "set-credentials", user,
"--token="+bearerToken)
}
mustExec(kubectl, "config", "set-context", context,
"--cluster="+domain,
"--user="+user,
"--namespace=kube-system")
switchContext := config.SwitchKubeconfigContext
if os.Getenv("HUB_KUBECONFIG") != "" {
kubeconfig := os.Getenv("HUB_KUBECONFIG")
os.Setenv("KUBECONFIG", kubeconfig)
}
if !switchContext && os.Getenv("KUBECONFIG") != "" {
// Hub CTL extensions expects a private Kubeconfig with current-context set
outBytes, err := execOutput(kubectl, "config", "current-context")
out := string(outBytes)
if strings.Contains(out, "current-context is not set") {
switchContext = true
}
if !switchContext && err != nil {
if processErr, ok := err.(*exec.ExitError); ok && processErr.ExitCode() == 1 {
switchContext = true
}
}
}
if switchContext {
mustExec(kubectl, "config", "use-context", context)
}
if !keepPems {
for _, filename := range pemsWritten {
if err := os.Remove(filename); err != nil {
util.WarnOnce("Unable to remove `%s`: %v", filename, err)
} else if config.Debug {
log.Printf("Removed `%s`", filename)
}
}
}
}
func mayOutput(params parameters.LockedParameters,
outputs parameters.CapturedOutputs, component string, candidates ...string) (string, bool) {
for _, name := range candidates {
if component != "" {
qName := parameters.OutputQualifiedName(name, component)
output, exist := outputs[qName]
if exist {
return util.String(output.Value), true
}
}
param, exist := params[name]
if exist {
return util.String(param.Value), true
}
}
return "", false
}
func mustOutput(params parameters.LockedParameters,
outputs parameters.CapturedOutputs, component string, candidates ...string) string {
if value, exist := mayOutput(params, outputs, component, candidates...); exist {
return value
}
if component != "" {
log.Printf("Component `%s` provides no %v output(s), nor such stack parameters are found", component, candidates)
if len(outputs) > 0 {
log.Print("Outputs:")
parameters.PrintCapturedOutputs(outputs)
}
} else {
log.Printf("No %v stack parameter(s) are found", candidates)
}
os.Exit(1)
return ""
}
func writeFile(filename string, content string) {
file, err := os.Create(filename)
if err != nil {
log.Fatalf("Unable to open `%s` for write: %v", filename, err)
}
wrote, err := strings.NewReader(content).WriteTo(file)
if err != nil || wrote != int64(len(content)) {
file.Close()
log.Fatalf("Unable to write `%s`: %v", filename, err)
}
file.Close()
if config.Debug {
log.Printf("Wrote `%s`", filename)
}
}
func execOutput(name string, args ...string) ([]byte, error) {
if config.Trace {
log.Printf("Executing %s %v", name, args)
}
cmd := exec.Command(name, args...)
outBytes, err := cmd.CombinedOutput()
if err != nil || config.Debug {
log.Printf("%s output:\n%s", name, outBytes)
}
return outBytes, err
}
func mustExec(name string, args ...string) {
_, err := execOutput(name, args...)
if err != nil {
log.Fatalf("%s failed: %v", name, err)
}
}
func warnClusterCaCertMismatch(configFilename, searchContext, ca string) {
kubeconfig, err := readKubeconfig(configFilename)
if err != nil {
return
}
for _, context := range kubeconfig.Contexts {
if context.Name == searchContext {
clusterName := context.Context["cluster"]
for _, cluster := range kubeconfig.Clusters {
if cluster.Name == clusterName {
configCaBase64, exist := cluster.Cluster["certificate-authority-data"]
if exist && configCaBase64 != "" {
title := searchContext
if searchContext != clusterName {
title = fmt.Sprintf("%s (%s)", searchContext, clusterName)
}
configCa, err := base64.StdEncoding.DecodeString(configCaBase64)
if err != nil {
break
}
if string(configCa) != ca {
util.WarnOnce(`Kubeconfig %s CA certificate doesn't match stack Kubernetes CA certificate;
You may want to 'kubectl config delete-context %s' for Hub CTL to re-create it`,
title, searchContext)
} else {
if config.Trace {
log.Printf("Kubeconfig %s CA certificate do match stack Kubernetes CA - good", title)
}
}
}
break
}
}
break
}
}
}