Make resource definition available to dashboard (#3666)

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 <tegioz@icloud.com> Signed-off-by: Cintia
Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
Sergio C. Arteaga 2019-12-03 19:25:20 +01:00 committed by Carol A. Scott
parent 66a74b23a7
commit 78ed5f8883
7 changed files with 113 additions and 4 deletions

View File

@ -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
}

View File

@ -11,6 +11,7 @@ import (
const (
All = "all"
Authority = "authority"
CronJob = "cronjob"
DaemonSet = "daemonset"
Deployment = "deployment"
Job = "job"

View File

@ -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);

View File

@ -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),

View File

@ -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}`);
});
});
});

View File

@ -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)
}

View File

@ -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)