package k8s import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "time" healthcheckPb "github.com/runconduit/conduit/controller/gen/common/healthcheck" "github.com/runconduit/conduit/pkg/healthcheck" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/rest" // Load all the auth plugins for the cloud providers. _ "k8s.io/client-go/plugin/pkg/client/auth" ) const ( KubeapiSubsystemName = "kubernetes-api" KubeapiClientCheckDescription = "can initialize the client" KubeapiAccessCheckDescription = "can query the Kubernetes API" KubeapiVersionCheckDescription = "is running the minimum Kubernetes API version" ) var minApiVersion = [3]int{1, 8, 0} type KubernetesApi interface { UrlFor(namespace string, extraPathStartingWithSlash string) (*url.URL, error) NewClient() (*http.Client, error) healthcheck.StatusChecker } type kubernetesApi struct { *rest.Config } func (kubeapi *kubernetesApi) NewClient() (*http.Client, error) { secureTransport, err := rest.TransportFor(kubeapi.Config) if err != nil { return nil, fmt.Errorf("error instantiating Kubernetes API client: %v", err) } return &http.Client{ Transport: secureTransport, }, nil } func (kubeapi *kubernetesApi) SelfCheck() (checks []*healthcheckPb.CheckResult) { apiConnectivityCheck, client := kubeapi.checkApiConnectivity() checks = append(checks, apiConnectivityCheck) if apiConnectivityCheck.Status != healthcheckPb.CheckStatus_OK { return } apiAccessCheck, versionRsp := kubeapi.checkApiAccess(client) checks = append(checks, apiAccessCheck) if apiAccessCheck.Status != healthcheckPb.CheckStatus_OK { return } checks = append(checks, kubeapi.checkApiVersion(versionRsp)) return } func (kubeapi *kubernetesApi) checkApiConnectivity() (*healthcheckPb.CheckResult, *http.Client) { checkResult := &healthcheckPb.CheckResult{ Status: healthcheckPb.CheckStatus_OK, SubsystemName: KubeapiSubsystemName, CheckDescription: KubeapiClientCheckDescription, } client, err := kubeapi.NewClient() if err != nil { checkResult.Status = healthcheckPb.CheckStatus_ERROR checkResult.FriendlyMessageToUser = fmt.Sprintf("Error connecting to the API. Error message is [%s]", err.Error()) return checkResult, client } return checkResult, client } func (kubeapi *kubernetesApi) checkApiAccess(client *http.Client) (*healthcheckPb.CheckResult, string) { checkResult := &healthcheckPb.CheckResult{ Status: healthcheckPb.CheckStatus_OK, SubsystemName: KubeapiSubsystemName, CheckDescription: KubeapiAccessCheckDescription, } endpointToCheck, err := url.Parse(kubeapi.Host + "/version") if err != nil { checkResult.Status = healthcheckPb.CheckStatus_ERROR checkResult.FriendlyMessageToUser = fmt.Sprintf("Error querying Kubernetes API. Configured host is [%s], error message is [%s]", kubeapi.Host, err.Error()) return checkResult, "" } req, _ := http.NewRequest("GET", endpointToCheck.String(), nil) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := client.Do(req.WithContext(ctx)) if err != nil { checkResult.Status = healthcheckPb.CheckStatus_ERROR checkResult.FriendlyMessageToUser = fmt.Sprintf("HTTP GET request to endpoint [%s] resulted in error: [%s]", endpointToCheck, err.Error()) return checkResult, "" } defer resp.Body.Close() bytes, err := ioutil.ReadAll(resp.Body) if err != nil { checkResult.Status = healthcheckPb.CheckStatus_ERROR checkResult.FriendlyMessageToUser = fmt.Sprintf("HTTP GET request to endpoint [%s] resulted in invalid response: [%v]", endpointToCheck, resp) return checkResult, "" } body := string(bytes) statusCodeReturnedIsWithinSuccessRange := resp.StatusCode < 400 if !statusCodeReturnedIsWithinSuccessRange { checkResult.Status = healthcheckPb.CheckStatus_FAIL checkResult.FriendlyMessageToUser = fmt.Sprintf("HTTP GET request to endpoint [%s] resulted in Status: [%s], body: [%s]", endpointToCheck, resp.Status, body) return checkResult, "" } return checkResult, body } func (kubeapi *kubernetesApi) checkApiVersion(versionRsp string) *healthcheckPb.CheckResult { checkResult := &healthcheckPb.CheckResult{ Status: healthcheckPb.CheckStatus_OK, SubsystemName: KubeapiSubsystemName, CheckDescription: KubeapiVersionCheckDescription, } var versionInfo version.Info err := json.Unmarshal([]byte(versionRsp), &versionInfo) if err != nil { checkResult.Status = healthcheckPb.CheckStatus_ERROR checkResult.FriendlyMessageToUser = fmt.Sprintf("Version endpoint returned invalid JSON: [%v]", versionRsp) return checkResult } apiVersion, err := getK8sVersion(versionInfo.String()) if err != nil { checkResult.Status = healthcheckPb.CheckStatus_ERROR checkResult.FriendlyMessageToUser = fmt.Sprintf("Failed to parse version [%s]: %s", versionInfo.String(), err) return checkResult } if !isCompatibleVersion(minApiVersion, apiVersion) { checkResult.Status = healthcheckPb.CheckStatus_FAIL checkResult.FriendlyMessageToUser = fmt.Sprintf("Kubernetes is on version [%d.%d.%d], but version [%d.%d.%d] or more recent is required.", apiVersion[0], apiVersion[1], apiVersion[2], minApiVersion[0], minApiVersion[1], minApiVersion[2]) return checkResult } return checkResult } // UrlFor generates a URL based on the Kubernetes config. func (kubeapi *kubernetesApi) UrlFor(namespace string, extraPathStartingWithSlash string) (*url.URL, error) { return generateKubernetesApiBaseUrlFor(kubeapi.Host, namespace, extraPathStartingWithSlash) } // NewAPI returns a new KubernetesApi interface func NewAPI(configPath string) (KubernetesApi, error) { config, err := getConfig(configPath) if err != nil { return nil, fmt.Errorf("error configuring Kubernetes API client: %v", err) } return &kubernetesApi{Config: config}, nil }