mirror of https://github.com/linkerd/linkerd2.git
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:
parent
66a74b23a7
commit
78ed5f8883
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
const (
|
||||
All = "all"
|
||||
Authority = "authority"
|
||||
CronJob = "cronjob"
|
||||
DaemonSet = "daemonset"
|
||||
Deployment = "deployment"
|
||||
Job = "job"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue