cloudfoundry-incubator/stratos

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

Summary

Maintainability
B
6 hrs
Test Coverage
package dashboard

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"

    "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
    log "github.com/sirupsen/logrus"
    "gopkg.in/yaml.v2"
)

const dashboardInstallYAMLDownloadURL = "https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.3/aio/deploy/recommended.yaml"

// Service Account definition - as per kube dashboard docs
const serviceAccountDefinition = `{
    "apiVersion": "v1",
    "kind": "ServiceAccount",
    "metadata": {
        "name": "stratos-dashboard-user",
        "namespace": "$NAMESPACE",
        "labels": {
            "stratos-role": "kubernetes-dashboard-user"
        }
    }
}`

// Cluster Role Binding definition - as per kube dashboard docs
const clusterRoleBindingDefinition = `{
    "apiVersion": "rbac.authorization.k8s.io/v1",
    "kind": "ClusterRoleBinding",
    "metadata": {
        "name": "stratos-dashboard-user",
        "labels": {
            "stratos-role": "kubernetes-dashboard-user"
        }
    },
    "roleRef": {
        "apiGroup": "rbac.authorization.k8s.io",
        "kind": "ClusterRole",
        "name": "cluster-admin"
    },
    "subjects": [
        {
            "kind": "ServiceAccount",
            "name": "stratos-dashboard-user",
            "namespace": "$NAMESPACE"
        }
    ]
}`

type apiVersionAndKind struct {
    APIVersion string `json:"apiVersion"`
    Kind       string `json:"kind"`
}

// CreateServiceAccount will create a service account for accessing the Kubernetes Dashboard
func CreateServiceAccount(p interfaces.PortalProxy, endpointGUID, userGUID string) error {
    log.Debug("CreateServiceAccount")

    svc, err := getKubeDashboardServiceInfo(p, endpointGUID, userGUID)
    if err != nil {
        return err
    }

    namespace := svc.Namespace
    target := fmt.Sprintf("api/v1/namespaces/%s/serviceaccounts", namespace)
    headers := make(http.Header, 0)
    headers.Set("Content-Type", "application/json")

    response, err := p.DoProxySingleRequest(endpointGUID, userGUID, "POST", target, headers,
        replaceNamespace(serviceAccountDefinition, namespace))
    if err != nil {
        return err
    }

    if response.StatusCode != 201 {
        return fmt.Errorf("Unable to create Service Account - unexpected response from API: %d", response.StatusCode)
    }

    target = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"
    response, err = p.DoProxySingleRequest(endpointGUID, userGUID, "POST", target, headers,
        replaceNamespace(clusterRoleBindingDefinition, namespace))
    if err != nil {
        return err
    }

    if response.StatusCode != 201 {
        return fmt.Errorf("Unable to create Cluster Role Binding - unexpected response from API: %d", response.StatusCode)
    }

    return err
}

func replaceNamespace(definition, namespace string) []byte {
    updated := strings.ReplaceAll(definition, "$NAMESPACE", namespace)
    return []byte(updated)
}

// DeleteServiceAccount will delete the service account
func DeleteServiceAccount(p interfaces.PortalProxy, endpointGUID, userGUID string) error {
    log.Debug("DeleteServiceAccount")

    svcAccount, err := getKubeDashboardServiceAccount(p, endpointGUID, userGUID, stratosServiceAccountSelector)
    if err != nil {
        return err
    }

    msg := ""
    target := fmt.Sprintf("api/v1/namespaces/%s/serviceaccounts/%s", svcAccount.Namespace, svcAccount.Name)
    response, err := p.DoProxySingleRequest(endpointGUID, userGUID, "DELETE", target, nil, nil)
    msg = addErrorMessage(msg, "Unable to delete Service Account", response, err)

    target = fmt.Sprintf("apis/rbac.authorization.k8s.io/v1/clusterrolebindings/%s", svcAccount.Name)
    response, err = p.DoProxySingleRequest(endpointGUID, userGUID, "DELETE", target, nil, nil)
    msg = addErrorMessage(msg, "Unable to delete Cluster Role Binding", response, err)

    if len(msg) > 0 {
        return errors.New(msg)
    }

    return nil
}

func addErrorMessage(msg, prefix string, response *interfaces.CNSIRequest, err error) string {
    errMsg := ""
    if err != nil {
        errMsg = fmt.Sprintf("%s - Error: %v", prefix, err.Error())
    } else if response.StatusCode != 200 {
        errMsg = fmt.Sprintf("%s - unexpected response from API: %d", prefix, response.StatusCode)
    }

    if len(errMsg) > 0 {
        if len(msg) > 0 {
            return fmt.Sprintf("%s. %s", msg, errMsg)
        }
        return errMsg
    }

    return msg
}

// InstallDashboard will install the dashboard into a Kubernetes cluster
func InstallDashboard(p interfaces.PortalProxy, endpointGUID, userGUID string) error {
    // Download the Yaml for the dashboard
    kubeDashboardImage := p.Env().String("STRATOS_KUBERNETES_DASHBOARD_IMAGE", "")
    if len(kubeDashboardImage) == 0 {
        kubeDashboardImage = dashboardInstallYAMLDownloadURL
    }

    log.Debugf("InstallDashboard: %s", kubeDashboardImage)

    http := p.GetHttpClient(false)
    resp, err := http.Get(kubeDashboardImage)
    if err != nil {
        return fmt.Errorf("Could not download YAML to install the dashboard: %+v", err)
    }
    if resp.StatusCode != 200 {
        return fmt.Errorf("Could not download YAML to install the dashboard: %s", resp.Status)
    }

    defer resp.Body.Close()

    // Read the entire body
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("Could not read YAML to install the dashboard: %s", err.Error())
    }

    r := bytes.NewReader(body)
    dec := yaml.NewDecoder(r)
    var t interface{}
    for dec.Decode(&t) == nil {
        jsonDoc, err := YAMLToJSONWithLabel(t)
        if err != nil {
            return fmt.Errorf("Could not convert YAML to JSON during dashboard installation %s", err.Error())
        }

        info := &apiVersionAndKind{}
        if err := json.Unmarshal(jsonDoc, info); err != nil {
            return fmt.Errorf("Could not parse YAML during dashboard installation %s", err.Error())
        }

        // Now create the resource
        var api, resource string
        if info.APIVersion != "v1" {
            api = fmt.Sprintf("apis/%s", info.APIVersion)
        } else {
            api = fmt.Sprintf("api/%s", info.APIVersion)
        }

        if isClusterAPI(info.Kind) {
            if info.Kind == "Namespace" {
                resource = fmt.Sprintf("api/v1/namespaces")
            } else {
                resource = fmt.Sprintf("%s/%ss", api, strings.ToLower(info.Kind))
            }
        } else {
            resource = fmt.Sprintf("%s/namespaces/kubernetes-dashboard/%ss", api, strings.ToLower(info.Kind))
        }

        response, err := p.DoProxySingleRequest(endpointGUID, userGUID, "POST", resource, nil, jsonDoc)
        if err != nil {
            return err
        }

        if response.StatusCode != 201 {
            // Don't fail if creation of a cluster-level resoures fails beacuse it already exists
            if !(response.StatusCode == 409 && isClusterAPI(info.Kind)) {
                return fmt.Errorf("Unable to delete %s - unexpected response from API: %d", info.Kind, response.StatusCode)
            }
        }
    }

    return nil
}

func isClusterAPI(api string) bool {
    return strings.HasPrefix(api, "Cluster") || api == "Namespace"
}

// DeleteDashboard will delete the dashboard from Kubernetes cluster
func DeleteDashboard(p interfaces.PortalProxy, endpointGUID, userGUID string) error {
    log.Debug("DeleteDashboard")

    // Delete the service
    svc, err := getKubeDashboardServiceInfo(p, endpointGUID, userGUID)
    if err == nil {
        svcTarget := fmt.Sprintf("api/v1/namespaces/%s/services/%s", svc.Namespace, svc.ServiceName)
        // Don't wory if this fails, it will get deleted when the namespace is deleted
        // We delete it here specifically so we know that it has gone since this is what we use
        // to determine if the Dashboard is installed
        p.DoProxySingleRequest(endpointGUID, userGUID, "DELETE", svcTarget, nil, nil)
    }

    // Delete the service account
    DeleteServiceAccount(p, endpointGUID, userGUID)

    // Delete the namespace 'kubernetes-dashboard'
    target := "api/v1/namespaces/kubernetes-dashboard?propagationPolicy=Background"
    _, err = p.DoProxySingleRequest(endpointGUID, userGUID, "DELETE", target, nil, nil)
    if err != nil {
        return err
    }

    return nil
}