linkerd2/pkg/k8s/api.go

175 lines
5.7 KiB
Go

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
}