From 78ed5f8883c4f8a7f6e41582666adb4f58326eab Mon Sep 17 00:00:00 2001 From: "Sergio C. Arteaga" Date: Tue, 3 Dec 2019 19:25:20 +0100 Subject: [PATCH] Make resource definition available to dashboard (#3666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR allows the dashboard to query for a resource's definition in YAML format, if the boolean `queryForDefinition` in the `ResourceDetail` component is set to true. This change to the web API and the dashboard component was made for a future redesigned dashboard detail page. At present, `queryForDefinition` is set to false and there is no visible change to the user with this PR. Signed-off-by: Sergio CastaƱo Arteaga Signed-off-by: Cintia Sanchez Garcia --- pkg/k8s/api.go | 7 +++ pkg/k8s/k8s.go | 1 + web/app/js/components/ResourceDetail.jsx | 25 ++++++-- web/app/js/components/util/ApiHelpers.jsx | 14 +++++ .../js/components/util/ApiHelpers.test.jsx | 11 ++++ web/srv/api_handlers.go | 58 +++++++++++++++++++ web/srv/server.go | 1 + 7 files changed, 113 insertions(+), 4 deletions(-) diff --git a/pkg/k8s/api.go b/pkg/k8s/api.go index e52b40554..20f6945a5 100644 --- a/pkg/k8s/api.go +++ b/pkg/k8s/api.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + tsclient "github.com/deislabs/smi-sdk-go/pkg/gen/client/split/clientset/versioned" "github.com/linkerd/linkerd2/pkg/prometheus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -30,6 +31,7 @@ type KubernetesAPI struct { *rest.Config kubernetes.Interface Apiextensions apiextensionsclient.Interface // for CRDs + TsClient tsclient.Interface } // NewAPI validates a Kubernetes config and returns a client for accessing the @@ -61,11 +63,16 @@ func NewAPI(configPath, kubeContext string, impersonate string, timeout time.Dur if err != nil { return nil, fmt.Errorf("error configuring Kubernetes API Extensions clientset: %v", err) } + tsClient, err := tsclient.NewForConfig(config) + if err != nil { + return nil, err + } return &KubernetesAPI{ Config: config, Interface: clientset, Apiextensions: apiextensions, + TsClient: tsClient, }, nil } diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index 28e4ab705..0485927a4 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -11,6 +11,7 @@ import ( const ( All = "all" Authority = "authority" + CronJob = "cronjob" DaemonSet = "daemonset" Deployment = "deployment" Job = "job" diff --git a/web/app/js/components/ResourceDetail.jsx b/web/app/js/components/ResourceDetail.jsx index 7708ec988..8807e3dd8 100644 --- a/web/app/js/components/ResourceDetail.jsx +++ b/web/app/js/components/ResourceDetail.jsx @@ -84,7 +84,11 @@ export class ResourceDetailBase extends React.Component { resourceIsMeshed: true, pendingRequests: false, loaded: false, - error: null + error: null, + resourceDefinition: null, + // queryForDefinition is set to false now due to we are not currently using + // resource definition. This can change in the future + queryForDefinition: false, }; } @@ -152,6 +156,11 @@ export class ResourceDetailBase extends React.Component { ) ]; + if (this.state.queryForDefinition) { + // definition for this resource + apiRequests.push(this.api.fetchResourceDefinition(resource.namespace, resource.type, resource.name)); + } + if (_indexOf(edgeDataAvailable, resource.type) > 0) { apiRequests = apiRequests.concat([ this.api.fetchEdges(resource.namespace, resource.type) @@ -161,12 +170,19 @@ export class ResourceDetailBase extends React.Component { this.api.setCurrentRequests(apiRequests); Promise.all(this.api.getCurrentPromises()) - .then(([resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp, edgesRsp]) => { + .then(results => { + const [resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp, ...rsp] = [...results]; let resourceMetrics = processSingleResourceRollup(resourceRsp, resource.type); let podMetrics = processSingleResourceRollup(podMetricsRsp, resource.type); let upstreamMetrics = processMultiResourceRollup(upstreamRsp, resource.type); let downstreamMetrics = processMultiResourceRollup(downstreamRsp, resource.type); - let edges = processEdges(edgesRsp, this.state.resource.name); + let resourceDefinition = this.state.queryForDefinition ? rsp[0] : this.state.resourceDefinition; + + let edges = []; + if (_indexOf(edgeDataAvailable, resource.type) > 0) { + const edgesRsp = rsp[rsp.length - 1]; + edges = processEdges(edgesRsp, this.state.resource.name); + } // INEFFICIENT: get metrics for all the pods belonging to this resource. // Do this by querying for metrics for all pods in this namespace and then filtering @@ -233,7 +249,8 @@ export class ResourceDetailBase extends React.Component { loaded: true, pendingRequests: false, error: null, - unmeshedSources: this.unmeshedSources // in place of debouncing, just update this when we update the rest of the state + unmeshedSources: this.unmeshedSources, // in place of debouncing, just update this when we update the rest of the state + resourceDefinition, // eslint-disable-line react/no-unused-state }); }) .catch(this.handleApiError); diff --git a/web/app/js/components/util/ApiHelpers.jsx b/web/app/js/components/util/ApiHelpers.jsx index d13a8ccc1..220346867 100644 --- a/web/app/js/components/util/ApiHelpers.jsx +++ b/web/app/js/components/util/ApiHelpers.jsx @@ -77,6 +77,15 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => { return makeCancelable(fetch(path), r => r.json()); }; + // for getting yaml api results + const apiFetchYAML = path => { + if (!_isEmpty(pathPrefix)) { + path = `${pathPrefix}${path}`; + } + + return makeCancelable(fetch(path), r => r.text()); + }; + // for getting non-json results const prefixedUrl = path => { if (!_isEmpty(pathPrefix)) { @@ -119,6 +128,10 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => { return apiFetch('/api/check'); }; + const fetchResourceDefinition = (namespace, resourceType, resourceName) => { + return apiFetchYAML(`/api/resource-definition?namespace=${namespace}&resource_type=${resourceType}&resource_name=${resourceName}`); + }; + const getMetricsWindow = () => metricsWindow; const getMetricsWindowDisplayText = () => validMetricsWindows[metricsWindow]; @@ -235,6 +248,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => { fetchServices, fetchEdges, fetchCheck, + fetchResourceDefinition, getMetricsWindow, setMetricsWindow, getValidMetricsWindows: () => Object.keys(validMetricsWindows), diff --git a/web/app/js/components/util/ApiHelpers.test.jsx b/web/app/js/components/util/ApiHelpers.test.jsx index 7cbb06a56..aee6ab9e2 100644 --- a/web/app/js/components/util/ApiHelpers.test.jsx +++ b/web/app/js/components/util/ApiHelpers.test.jsx @@ -303,4 +303,15 @@ describe('ApiHelpers', () => { expect(fetchStub.args[0][0]).toEqual('/api/check'); }); }); + + describe('fetchResourceDefinition', () => { + it('fetches the resource definition from the api', () => { + const [namespace, type, name] = ["namespace", "type", "name"]; + api = ApiHelpers(); + api.fetchResourceDefinition(namespace, type, name); + + expect(fetchStub.calledOnce).toBeTruthy; + expect(fetchStub.args[0][0]).toEqual(`/api/resource-definition?namespace=${namespace}&resource_type=${type}&resource_name=${name}`); + }); + }); }); diff --git a/web/srv/api_handlers.go b/web/srv/api_handlers.go index cd0b57a2b..a2553d130 100644 --- a/web/srv/api_handlers.go +++ b/web/srv/api_handlers.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "regexp" + "strings" "time" "github.com/golang/protobuf/jsonpb" @@ -21,6 +22,8 @@ import ( "github.com/linkerd/linkerd2/pkg/protohttp" "github.com/linkerd/linkerd2/pkg/tap" log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" ) // Control Frame payload size can be no bigger than 125 bytes. 2 bytes are @@ -358,3 +361,58 @@ func (h *handler) handleAPICheck(w http.ResponseWriter, req *http.Request, p htt "results": results, }) } + +func (h *handler) handleAPIResourceDefinition(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { + var missingParams []string + requiredParams := []string{"namespace", "resource_type", "resource_name"} + for _, param := range requiredParams { + if req.FormValue(param) == "" { + missingParams = append(missingParams, param) + } + } + if len(missingParams) != 0 { + renderJSONError(w, fmt.Errorf("Required params not provided: %s", strings.Join(missingParams, ", ")), http.StatusBadRequest) + return + } + + namespace := req.FormValue("namespace") + resourceType := req.FormValue("resource_type") + resourceName := req.FormValue("resource_name") + + var resource interface{} + var err error + options := metav1.GetOptions{} + switch resourceType { + case k8s.CronJob: + resource, err = h.k8sAPI.BatchV1beta1().CronJobs(namespace).Get(resourceName, options) + case k8s.DaemonSet: + resource, err = h.k8sAPI.AppsV1().DaemonSets(namespace).Get(resourceName, options) + case k8s.Deployment: + resource, err = h.k8sAPI.AppsV1().Deployments(namespace).Get(resourceName, options) + case k8s.Job: + resource, err = h.k8sAPI.BatchV1().Jobs(namespace).Get(resourceName, options) + case k8s.Pod: + resource, err = h.k8sAPI.CoreV1().Pods(namespace).Get(resourceName, options) + case k8s.ReplicationController: + resource, err = h.k8sAPI.CoreV1().ReplicationControllers(namespace).Get(resourceName, options) + case k8s.ReplicaSet: + resource, err = h.k8sAPI.AppsV1().ReplicaSets(namespace).Get(resourceName, options) + case k8s.TrafficSplit: + resource, err = h.k8sAPI.TsClient.SplitV1alpha1().TrafficSplits(namespace).Get(resourceName, options) + default: + renderJSONError(w, errors.New("Invalid resource type: "+resourceType), http.StatusBadRequest) + return + } + if err != nil { + renderJSONError(w, err, http.StatusInternalServerError) + return + } + + resourceDefinition, err := yaml.Marshal(resource) + if err != nil { + renderJSONError(w, err, http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/yaml") + w.Write(resourceDefinition) +} diff --git a/web/srv/server.go b/web/srv/server.go index c7b7694c3..4ea738f2b 100644 --- a/web/srv/server.go +++ b/web/srv/server.go @@ -173,6 +173,7 @@ func NewServer( server.router.GET("/api/routes", handler.handleAPITopRoutes) server.router.GET("/api/edges", handler.handleAPIEdges) server.router.GET("/api/check", handler.handleAPICheck) + server.router.GET("/api/resource-definition", handler.handleAPIResourceDefinition) // grafana proxy server.router.DELETE("/grafana/*grafanapath", handler.handleGrafana)