cloudfoundry-incubator/stratos

View on GitHub
src/jetstream/plugins/kubernetes/dashboard/common.go

Summary

Maintainability
B
5 hrs
Test Coverage
package dashboard

import (
    "encoding/json"
    "errors"
    "fmt"
    "time"

    "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
    "github.com/labstack/echo/v4"
    log "github.com/sirupsen/logrus"

    v1 "k8s.io/api/core/v1"
)

const (
    kubeDashSessionGroup       = "kubernetes-dashboard"
    kubeDashSessionEndpointID  = "kubeDashSessionEndpointID"
    kubeDashSessionNamespace   = "kubeDashSessionNamespace"
    kubeDashSessionScheme      = "kubeDashSessionScheme"
    kubeDashSessionServiceName = "kubeDashSessionServiceName"
    kubeDashSessionToken       = "kubeDashSessionToken"

    defaultFlushInterval = 200 * time.Millisecond
)

// ServiceInfo represents the information for the Dashboard Service
// that we need to proxy the service
type ServiceInfo struct {
    Namespace        string `json:"namespace"`
    ServiceName      string `json:"name"`
    Scheme           string `json:"scheme"`
    StratosInstalled bool   `json:"-"`
}

// StatusResponse is the response from the dashboard status check
type StatusResponse struct {
    Endpoint         string             `json:"guid"`
    Installed        bool               `json:"installed"`
    StratosInstalled bool               `json:"stratosInstalled"`
    Running          bool               `json:"running"`
    Pod              *v1.Pod            `json:"pod"`
    Version          string             `json:"version"`
    Service          *ServiceInfo       `json:"service"`
    HasToken         bool               `json:"tokenExists"`
    ServiceAccont    *v1.ServiceAccount `json:"serviceAccount"`
    Token            string             `json:"-"`
}

// Determine if the specified Kube endpoint has the dashboard installed and ready
func getKubeDashboardPod(p interfaces.PortalProxy, cnsiGUID, userGUID string, labelSelector string) (*v1.Pod, error) {
    log.Debug("kubeDashboardStatus request")

    response, err := p.DoProxySingleRequest(cnsiGUID, userGUID, "GET", "/api/v1/pods?labelSelector="+labelSelector, nil, nil)
    if err != nil || response.StatusCode != 200 {
        return nil, errors.New("Could not fetch pod list")
    }

    ok, list, err := tryDecodePodList(response.Response)
    if !ok {
        return nil, errors.New("Kube dashboard not installed - could not decode pod list")
    }

    if len(list.Items) == 0 {
        return nil, errors.New("Kube dashboard not installed")
    }

    // Should just be one pod
    if len(list.Items) > 1 {
        return nil, errors.New("More than one Kubernetes Dashboard installation found")
    }

    pod := list.Items[0]
    return &pod, nil
}

// Get the service for the kubernetes dashboard
func getKubeDashboardService(p interfaces.PortalProxy, cnsiGUID, userGUID string, labelSelector string) (ServiceInfo, error) {
    log.Debug("getKubeDashboardService request")

    info := ServiceInfo{}
    response, err := p.DoProxySingleRequest(cnsiGUID, userGUID, "GET", "/api/v1/services?labelSelector="+labelSelector, nil, nil)
    if err != nil || response.StatusCode != 200 {
        return info, errors.New("Could not fetch service list")
    }

    ok, list, err := tryDecodeServiceList(response.Response)
    if !ok {
        return info, errors.New("Kube dashboard not installed - could not decode service list")
    }

    if len(list.Items) == 0 {
        return info, errors.New("Kube dashboard not installed")
    }

    // Should just be one pod
    if len(list.Items) != 1 {
        return info, errors.New("Kube dashboard not installed - too many pods")
    }

    svc := list.Items[0]
    info.Namespace = svc.Namespace
    info.ServiceName = svc.Name
    info.Scheme = "http"

    if len(svc.Spec.Ports) > 0 {
        port := svc.Spec.Ports[0].Port
        if port == 443 {
            info.Scheme = "https"
        }
    }

    // Check the labels on the service
    info.StratosInstalled = hasAnnotation(svc.Labels, "stratos-role", "kubernetes-dashboard")

    return info, nil
}

func getKubeDashboardServiceInfo(p interfaces.PortalProxy, endpointGUID, userGUID string) (ServiceInfo, error) {
    svc, err := getKubeDashboardService(p, endpointGUID, userGUID, "app%3Dkubernetes-dashboard")
    if err != nil {
        svc, err = getKubeDashboardService(p, endpointGUID, userGUID, "k8s-app%3Dkubernetes-dashboard")
    }
    return svc, err
}

// Get the service account for the kubernetes dashboard
func getKubeDashboardServiceAccount(p interfaces.PortalProxy, cnsiGUID, userGUID string, labelSelector string) (*v1.ServiceAccount, error) {
    log.Debug("getKubeDashboardService request")

    response, err := p.DoProxySingleRequest(cnsiGUID, userGUID, "GET", "/api/v1/serviceaccounts?labelSelector="+labelSelector, nil, nil)
    if err != nil || response.StatusCode != 200 {
        return nil, errors.New("Could not fetch service account list")
    }

    ok, list, err := tryDecodeServiceAccountList(response.Response)
    if !ok {
        return nil, errors.New("Could not find service account for Kubernetes dashboard")
    }

    if len(list.Items) == 0 {
        return nil, errors.New("Could not find service account for Kubernetes dashboard")
    }

    // Should just be one pod
    if len(list.Items) != 1 {
        return nil, errors.New("Could not find service account for Kubernetes dashboard - too may accounts")
    }

    svcAccount := list.Items[0]
    return &svcAccount, nil
}

// Get the service account for the kubernetes dashboard
func getKubeDashboardSecretToken(p interfaces.PortalProxy, cnsiGUID, userGUID string, sa *v1.ServiceAccount) (string, error) {
    log.Debug("getKubeDashboardSecretToken request")

    namespace := sa.Namespace

    if len(sa.Secrets) != 1 {
        return "", errors.New("Service Account has too many secrets - expecting only 1")
    }

    // Need to get all secrets in the namespace and find the one with the correct annotation
    apiURL := fmt.Sprintf("/api/v1/namespaces/%s/secrets", namespace)
    response, err := p.DoProxySingleRequest(cnsiGUID, userGUID, "GET", apiURL, nil, nil)
    if err != nil || response.StatusCode != 200 {
        return "", errors.New("Could not find secrets for Kubernetes dashboard")
    }

    ok, secrets, err := tryDecodeSecrets(response.Response)
    if !ok {
        return "", errors.New("Could not find secrets for Kubernetes dashboard")
    }

    for _, secret := range secrets.Items {
        if hasAnnotation(secret.Annotations, "kubernetes.io/service-account.name", sa.Name) {
            if token, ok := secret.Data["token"]; ok {
                return string(token), nil
            }
            return "", errors.New("Could not find token in the data for the Service Account Secret")
        }
    }

    return "", errors.New("Could not find token for the Service Account")
}

// Check string map for the given (key, value) pair
// Used to check if an annotation with the specified value if present on a resource
func hasAnnotation(annotations map[string]string, key, value string) bool {
    for k, v := range annotations {
        if k == key && v == value {
            return true
        }
    }
    return false
}

func tryDecodePodList(data []byte) (bool, v1.PodList, error) {
    var pods v1.PodList
    var err error

    err = json.Unmarshal(data, &pods)
    if err != nil {
        return false, pods, err
    }
    return true, pods, err
}

func tryDecodeServiceList(data []byte) (bool, v1.ServiceList, error) {
    var svcs v1.ServiceList
    var err error

    err = json.Unmarshal(data, &svcs)
    if err != nil {
        return false, svcs, err
    }
    return true, svcs, err
}

func tryDecodeServiceAccountList(data []byte) (bool, v1.ServiceAccountList, error) {
    var svcAccounts v1.ServiceAccountList
    var err error

    err = json.Unmarshal(data, &svcAccounts)
    if err != nil {
        return false, svcAccounts, err
    }
    return true, svcAccounts, err
}

func tryDecodeSecrets(data []byte) (bool, v1.SecretList, error) {
    var secrets v1.SecretList
    var err error

    err = json.Unmarshal(data, &secrets)
    if err != nil {
        return false, secrets, err
    }
    return true, secrets, err
}

// Send an error page that will get loaded into the IFRAME and the onload handler will detect
// it and show a Stratos error message
func sendErrorPage(c echo.Context, msg string) error {
    html := fmt.Sprintf("<html><body><stratos-error>%s</stratos-error></body></html>", msg)
    c.Response().Write([]byte(html))
    return nil
}