Wire up stats commands for daemonsets (#2006) (#2086)

DaemonSet stats are not currently shown in the cli stat command, web ui
or grafana dashboard. This commit adds daemonset support for stat.

Update stat command's help message to reference daemonsets.
Update the public-api to support stats for daemonsets.
Add tests for stat summary and api.

Add daemonset get/list/watch permissions to the linkerd-controller
cluster role that's created using the install command.
Update golden expectation test files for install command
yaml manifest output.

Update web UI with daemonsets
Update navigation, overview and pages to list daemonsets and the pods
associated to them.
Add daemonset paths to server, and ui apps.

Add grafana dashboard for daemonsets; a clone of the deployment
dashboard.

Update dependencies and dockerfile hashes

Add DaemonSet support to tap and top commands

Fixes of #2006

Signed-off-by: Zak Knill <zrjknill@gmail.com>
This commit is contained in:
zak 2019-01-24 22:34:13 +00:00 committed by Andrew Seigner
parent 2217a6cd2b
commit 8c413ca38b
30 changed files with 2573 additions and 59 deletions

View File

@ -1052,6 +1052,7 @@
"k8s.io/client-go/discovery/fake",
"k8s.io/client-go/informers",
"k8s.io/client-go/informers/admissionregistration/v1beta1",
"k8s.io/client-go/informers/apps/v1",
"k8s.io/client-go/informers/apps/v1beta2",
"k8s.io/client-go/informers/core/v1",
"k8s.io/client-go/kubernetes",

View File

@ -1,5 +1,5 @@
## compile binaries
FROM gcr.io/linkerd-io/go-deps:8c5ab859 as golang
FROM gcr.io/linkerd-io/go-deps:02bd701f as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY cli cli
COPY controller/k8s controller/k8s

View File

@ -59,6 +59,7 @@ func newCmdStat() *cobra.Command {
Examples:
* deploy
* deploy/my-deploy
* ds/my-daemonset
* rc/my-replication-controller
* ns/my-ns
* authority
@ -69,6 +70,7 @@ func newCmdStat() *cobra.Command {
* all
Valid resource types include:
* daemonsets
* deployments
* namespaces
* pods

View File

@ -58,9 +58,11 @@ func newCmdTap() *cobra.Command {
* deploy
* deploy/my-deploy
* deploy my-deploy
* ds/my-daemonset
* ns/my-ns
Valid resource types include:
* daemonsets
* deployments
* namespaces
* pods

View File

@ -20,7 +20,7 @@ metadata:
name: linkerd-linkerd-controller
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments", "replicasets"]
resources: ["daemonsets", "deployments", "replicasets"]
verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"]

View File

@ -20,7 +20,7 @@ metadata:
name: linkerd-linkerd-controller
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments", "replicasets"]
resources: ["daemonsets", "deployments", "replicasets"]
verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"]

View File

@ -20,7 +20,7 @@ metadata:
name: linkerd-linkerd-controller
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments", "replicasets"]
resources: ["daemonsets", "deployments", "replicasets"]
verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"]

View File

@ -22,7 +22,7 @@ metadata:
name: linkerd-Namespace-controller
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments", "replicasets"]
resources: ["daemonsets", "deployments", "replicasets"]
verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"]

View File

@ -15,7 +15,7 @@ metadata:
namespace: Namespace
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments", "replicasets"]
resources: ["daemonsets", "deployments", "replicasets"]
verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods", "endpoints", "services", "replicationcontrollers"]

View File

@ -284,9 +284,11 @@ func newCmdTop() *cobra.Command {
* deploy
* deploy/my-deploy
* deploy my-deploy
* ds/my-daemonset
* ns/my-ns
Valid resource types include:
* daemonsets
* deployments
* namespaces
* pods

View File

@ -32,7 +32,7 @@ metadata:
{{- end}}
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments", "replicasets"]
resources: ["daemonsets", "deployments", "replicasets"]
verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods", "endpoints", "services", "replicationcontrollers"{{if not .SingleNamespace}}, "namespaces"{{end}}]

View File

@ -1,5 +1,5 @@
## compile controller services
FROM gcr.io/linkerd-io/go-deps:8c5ab859 as golang
FROM gcr.io/linkerd-io/go-deps:02bd701f as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY controller/gen controller/gen
COPY pkg pkg

View File

@ -81,6 +81,7 @@ func testStatSummary(t *testing.T, expectations []statSumExpected) {
}
rspStatTables := rsp.GetOk().StatTables
sort.Sort(byStatResult(rspStatTables))
if len(rspStatTables) != len(exp.expectedResponse.GetOk().StatTables) {
t.Fatalf(
@ -92,7 +93,6 @@ func testStatSummary(t *testing.T, expectations []statSumExpected) {
)
}
sort.Sort(byStatResult(rspStatTables))
statOkRsp := &pb.StatSummaryResponse_Ok{
StatTables: rspStatTables,
}
@ -209,6 +209,82 @@ status:
testStatSummary(t, expectations)
})
t.Run("Successfully performs a query based on resource type DaemonSet", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
expectedStatRPC: expectedStatRPC{
err: nil,
k8sConfigs: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: emoji
namespace: emojivoto
spec:
selector:
matchLabels:
app: emoji-svc
strategy: {}
template:
spec:
containers:
- image: buoyantio/emojivoto-emoji-svc:v3
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: linkerd
status:
phase: Running
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-not-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-not-running
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: linkerd
status:
phase: Completed
`,
},
mockPromResponse: prometheusMetric("emoji", "daemonset", "emojivoto", "success", false),
},
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: pkgK8s.DaemonSet,
},
},
TimeWindow: "1m",
},
expectedResponse: GenStatSummaryResponse("emoji", pkgK8s.DaemonSet, []string{"emojivoto"}, &PodCounts{
MeshedPods: 1,
RunningPods: 2,
FailedPods: 0,
}, true),
},
}
testStatSummary(t, expectations)
})
t.Run("Queries prometheus for a specific resource if name is specified", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
@ -629,6 +705,13 @@ status:
},
},
},
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{
Rows: []*pb.StatTable_PodGroup_Row{},
},
},
},
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{

View File

@ -2,6 +2,7 @@ package public
import (
"context"
"fmt"
"sort"
"testing"
@ -11,9 +12,8 @@ import (
"github.com/prometheus/common/model"
)
var booksConfig = []string{
// deployment/books
`kind: Deployment
// deployment/books
var booksDeployConfig = `kind: Deployment
apiVersion: apps/v1beta2
metadata:
name: books
@ -30,8 +30,28 @@ spec:
spec:
dnsPolicy: ClusterFirst
containers:
- image: buoyantio/booksapp:v0.0.2`,
- image: buoyantio/booksapp:v0.0.2`
// daemonset/books
var booksDaemonsetConfig = `kind: DaemonSet
apiVersion: apps/v1
metadata:
name: books
namespace: default
spec:
selector:
matchLabels:
app: books
template:
metadata:
labels:
app: books
spec:
dnsPolicy: ClusterFirst
containers:
- image: buoyantio/booksapp:v0.0.2`
var booksServiceConfig = []string{
// service/books
`apiVersion: v1
kind: Service
@ -71,6 +91,9 @@ spec:
`,
}
var booksConfig = append(booksServiceConfig, booksDeployConfig)
var booksDSConfig = append(booksServiceConfig, booksDaemonsetConfig)
type topRoutesExpected struct {
expectedStatRPC
req pb.TopRoutesRequest // the request we would like to test
@ -120,45 +143,46 @@ func genDefaultRouteSample() *model.Sample {
}
func testTopRoutes(t *testing.T, expectations []topRoutesExpected) {
for _, exp := range expectations {
mockProm, fakeGrpcServer, err := newMockGrpcServer(exp.expectedStatRPC)
if err != nil {
t.Fatalf("Error creating mock grpc server: %s", err)
}
rsp, err := fakeGrpcServer.TopRoutes(context.TODO(), &exp.req)
if err != exp.err {
t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
}
err = exp.verifyPromQueries(mockProm)
if err != nil {
t.Fatal(err)
}
rows := rsp.GetOk().GetRoutes()[0].Rows
if len(rows) != len(exp.expectedResponse.GetOk().GetRoutes()[0].Rows) {
t.Fatalf(
"Expected [%d] rows, got [%d].\nExpected:\n%s\nGot:\n%s",
len(exp.expectedResponse.GetOk().GetRoutes()[0].Rows),
len(rows),
exp.expectedResponse.GetOk().GetRoutes()[0].Rows,
rows,
)
}
sort.Slice(rows, func(i, j int) bool {
return rows[i].GetAuthority()+rows[i].GetRoute() < rows[j].GetAuthority()+rows[j].GetRoute()
})
for i, row := range rows {
expected := exp.expectedResponse.GetOk().GetRoutes()[0].Rows[i]
if !proto.Equal(row, expected) {
t.Fatalf("Expected: %+v\n Got: %+v", expected, row)
for id, exp := range expectations {
t.Run(fmt.Sprintf("%d", id), func(t *testing.T) {
mockProm, fakeGrpcServer, err := newMockGrpcServer(exp.expectedStatRPC)
if err != nil {
t.Fatalf("Error creating mock grpc server: %s", err)
}
}
rsp, err := fakeGrpcServer.TopRoutes(context.TODO(), &exp.req)
if err != exp.err {
t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
}
err = exp.verifyPromQueries(mockProm)
if err != nil {
t.Fatal(err)
}
rows := rsp.GetOk().GetRoutes()[0].Rows
if len(rows) != len(exp.expectedResponse.GetOk().GetRoutes()[0].Rows) {
t.Fatalf(
"Expected [%d] rows, got [%d].\nExpected:\n%s\nGot:\n%s",
len(exp.expectedResponse.GetOk().GetRoutes()[0].Rows),
len(rows),
exp.expectedResponse.GetOk().GetRoutes()[0].Rows,
rows,
)
}
sort.Slice(rows, func(i, j int) bool {
return rows[i].GetAuthority()+rows[i].GetRoute() < rows[j].GetAuthority()+rows[j].GetRoute()
})
for i, row := range rows {
expected := exp.expectedResponse.GetOk().GetRoutes()[0].Rows[i]
if !proto.Equal(row, expected) {
t.Fatalf("Expected: %+v\n Got: %+v", expected, row)
}
}
})
}
}
@ -235,6 +259,39 @@ func TestTopRoutes(t *testing.T) {
testTopRoutes(t, expectations)
})
t.Run("Successfully performs a routes query for a daemonset", func(t *testing.T) {
routes := []string{"/a"}
counts := []uint64{123}
expectations := []topRoutesExpected{
topRoutesExpected{
expectedStatRPC: expectedStatRPC{
err: nil,
mockPromResponse: routesMetric([]string{"/a"}),
expectedPrometheusQueries: []string{
`histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
`histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
`histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (le, dst, rt_route))`,
`sum(increase(route_response_total{daemonset="books", direction="inbound", dst=~"(books.default.svc.cluster.local)(:\\d+)?", namespace="default"}[1m])) by (rt_route, dst, classification)`,
},
k8sConfigs: booksDSConfig,
},
req: pb.TopRoutesRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: "default",
Type: pkgK8s.DaemonSet,
Name: "books",
},
},
TimeWindow: "1m",
},
expectedResponse: GenTopRoutesResponse(routes, counts, false, "books"),
},
}
testTopRoutes(t, expectations)
})
t.Run("Successfully performs an outbound routes query", func(t *testing.T) {
routes := []string{"/a"}
counts := []uint64{123}

View File

@ -28,6 +28,7 @@ var (
// destination resource on an outbound 'from' query
ValidTargets = []string{
k8s.Authority,
k8s.DaemonSet,
k8s.Deployment,
k8s.Namespace,
k8s.Pod,
@ -37,6 +38,7 @@ var (
// ValidTapDestinations specifies resource types allowed as a tap destination:
// destination resource on an outbound 'to' query
ValidTapDestinations = []string{
k8s.DaemonSet,
k8s.Deployment,
k8s.Job,
k8s.Namespace,

View File

@ -45,7 +45,7 @@ func main() {
var spClient *spclient.Clientset
restrictToNamespace := ""
resources := []k8s.APIResource{k8s.Deploy, k8s.Pod, k8s.RC, k8s.RS, k8s.Svc}
resources := []k8s.APIResource{k8s.DS, k8s.Deploy, k8s.Pod, k8s.RC, k8s.RS, k8s.Svc}
if *singleNamespace {
restrictToNamespace = *controllerNamespace

View File

@ -37,6 +37,7 @@ func main() {
k8sClient,
nil,
restrictToNamespace,
k8s.DS,
k8s.Deploy,
k8s.Pod,
k8s.RC,

View File

@ -13,6 +13,7 @@ import (
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
appsv1 "k8s.io/api/apps/v1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -20,7 +21,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
arinformers "k8s.io/client-go/informers/admissionregistration/v1beta1"
appinformers "k8s.io/client-go/informers/apps/v1beta2"
appv1informers "k8s.io/client-go/informers/apps/v1"
appv1beta2informers "k8s.io/client-go/informers/apps/v1beta2"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
@ -34,6 +36,7 @@ type APIResource int
const (
CM APIResource = iota
Deploy
DS
Endpoint
MWC // mutating webhook configuration
Pod
@ -48,12 +51,13 @@ type API struct {
Client kubernetes.Interface
cm coreinformers.ConfigMapInformer
deploy appinformers.DeploymentInformer
deploy appv1beta2informers.DeploymentInformer
ds appv1informers.DaemonSetInformer
endpoint coreinformers.EndpointsInformer
mwc arinformers.MutatingWebhookConfigurationInformer
pod coreinformers.PodInformer
rc coreinformers.ReplicationControllerInformer
rs appinformers.ReplicaSetInformer
rs appv1beta2informers.ReplicaSetInformer
sp spinformers.ServiceProfileInformer
svc coreinformers.ServiceInformer
@ -101,6 +105,9 @@ func NewAPI(k8sClient kubernetes.Interface, spClient spclient.Interface, namespa
case Deploy:
api.deploy = sharedInformers.Apps().V1beta2().Deployments()
api.syncChecks = append(api.syncChecks, api.deploy.Informer().HasSynced)
case DS:
api.ds = sharedInformers.Apps().V1().DaemonSets()
api.syncChecks = append(api.syncChecks, api.ds.Informer().HasSynced)
case Endpoint:
api.endpoint = sharedInformers.Core().V1().Endpoints()
api.syncChecks = append(api.syncChecks, api.endpoint.Informer().HasSynced)
@ -144,15 +151,23 @@ func (api *API) Sync() {
}
// Deploy provides access to a shared informer and lister for Deployments.
func (api *API) Deploy() appinformers.DeploymentInformer {
func (api *API) Deploy() appv1beta2informers.DeploymentInformer {
if api.deploy == nil {
panic("Deploy informer not configured")
}
return api.deploy
}
// DS provides access to a shared informer and lister for Daemonsets.
func (api *API) DS() appv1informers.DaemonSetInformer {
if api.ds == nil {
panic("DS informer not configured")
}
return api.ds
}
// RS provides access to a shared informer and lister for ReplicaSets.
func (api *API) RS() appinformers.ReplicaSetInformer {
func (api *API) RS() appv1beta2informers.ReplicaSetInformer {
if api.rs == nil {
panic("RS informer not configured")
}
@ -223,6 +238,8 @@ func (api *API) GetObjects(namespace, restype, name string) ([]runtime.Object, e
switch restype {
case k8s.Namespace:
return api.getNamespaces(name)
case k8s.DaemonSet:
return api.getDaemonsets(namespace, name)
case k8s.Deployment:
return api.getDeployments(namespace, name)
case k8s.Pod:
@ -271,6 +288,10 @@ func (api *API) GetPodsFor(obj runtime.Object, includeFailed bool) ([]*apiv1.Pod
namespace = typed.Name
selector = labels.Everything()
case *appsv1.DaemonSet:
namespace = typed.Namespace
selector = labels.Set(typed.Spec.Selector.MatchLabels).AsSelector()
case *appsv1beta2.Deployment:
namespace = typed.Namespace
selector = labels.Set(typed.Spec.Selector.MatchLabels).AsSelector()
@ -326,6 +347,9 @@ func GetNameAndNamespaceOf(obj runtime.Object) (string, string, error) {
case *apiv1.Namespace:
return typed.Name, typed.Name, nil
case *appsv1.DaemonSet:
return typed.Name, typed.Namespace, nil
case *appsv1beta2.Deployment:
return typed.Name, typed.Namespace, nil
@ -481,6 +505,32 @@ func (api *API) getRCs(namespace, name string) ([]runtime.Object, error) {
return objects, nil
}
func (api *API) getDaemonsets(namespace, name string) ([]runtime.Object, error) {
var err error
var daemonsets []*appsv1.DaemonSet
if namespace == "" {
daemonsets, err = api.DS().Lister().List(labels.Everything())
} else if name == "" {
daemonsets, err = api.DS().Lister().DaemonSets(namespace).List(labels.Everything())
} else {
var ds *appsv1.DaemonSet
ds, err = api.DS().Lister().DaemonSets(namespace).Get(name)
daemonsets = []*appsv1.DaemonSet{ds}
}
if err != nil {
return nil, err
}
objects := []runtime.Object{}
for _, ds := range daemonsets {
objects = append(objects, ds)
}
return objects, nil
}
func (api *API) getServices(namespace, name string) ([]runtime.Object, error) {
services, err := api.GetServices(namespace, name)

View File

@ -129,6 +129,39 @@ apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: my-deploy
namespace: not-my-ns`,
},
},
getObjectsExpected{
err: nil,
namespace: "",
resType: k8s.DaemonSet,
name: "",
k8sResResults: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-deploy
namespace: my-ns`,
},
},
getObjectsExpected{
err: nil,
namespace: "my-ns",
resType: k8s.DaemonSet,
name: "my-ds",
k8sResResults: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-ds
namespace: my-ns`,
},
k8sResMisc: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-ds
namespace: not-my-ns`,
},
},
@ -362,6 +395,31 @@ status:
getPodsForExpected{
err: nil,
k8sResInput: `
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: emoji
namespace: emojivoto
spec:
selector:
matchLabels:
app: emoji-svc`,
k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running`,
},
k8sResMisc: []string{},
},
getPodsForExpected{
err: nil,
k8sResInput: `
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:

View File

@ -42,6 +42,7 @@ func NewFakeAPI(namespace string, configs ...string) (*API, error) {
namespace,
CM,
Deploy,
DS,
Endpoint,
Pod,
RC,

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,7 @@ var AllResources = []string{
// StatAllResourceTypes represents the resources to query in StatSummary when Resource.Type is "all"
var StatAllResourceTypes = []string{
// TODO: add Namespace here to decrease queries from the web process
DaemonSet,
Deployment,
ReplicationController,
Pod,

View File

@ -1,5 +1,5 @@
## compile proxy-init utility
FROM gcr.io/linkerd-io/go-deps:8c5ab859 as golang
FROM gcr.io/linkerd-io/go-deps:02bd701f as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY ./proxy-init ./proxy-init
RUN CGO_ENABLED=0 GOOS=linux go install -v ./proxy-init/

View File

@ -26,7 +26,7 @@ COPY web/app .
RUN $ROOT/bin/web build
## compile go server
FROM gcr.io/linkerd-io/go-deps:8c5ab859 as golang
FROM gcr.io/linkerd-io/go-deps:02bd701f as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
RUN mkdir -p web
COPY web/main.go web

View File

@ -142,6 +142,7 @@ class Namespaces extends React.Component {
<NetworkGraph namespace={this.state.ns} deployments={metrics.deployment} />
}
{this.renderResourceSection("deployment", metrics.deployment)}
{this.renderResourceSection("daemonset", metrics.daemonset)}
{this.renderResourceSection("replicationcontroller", metrics.replicationcontroller)}
{this.renderResourceSection("pod", metrics.pod)}
{this.renderResourceSection("authority", metrics.authority)}

View File

@ -155,6 +155,7 @@ class NamespaceLanding extends React.Component {
<Grid item>{ noMetrics ? <div>No resources detected.</div> : null}</Grid>
{this.renderResourceSection("deployment", metrics.deployment)}
{this.renderResourceSection("daemonset", metrics.daemonset)}
{this.renderResourceSection("replicationcontroller", metrics.replicationcontroller)}
{this.renderResourceSection("pod", metrics.pod)}
{this.renderResourceSection("authority", metrics.authority)}

View File

@ -69,6 +69,7 @@ class NavigationResourcesBase extends React.Component {
<MenuList dense component="div" disablePadding>
<NavigationResource type="authorities" />
<NavigationResource type="deployments" metrics={allMetrics.deployment} />
<NavigationResource type="daemonsets" metrics={allMetrics.daemonset} />
<NavigationResource type="namespaces" metrics={nsMetrics.namespace} />
<NavigationResource type="pods" metrics={allMetrics.pod} />
<NavigationResource type="replicationcontrollers" metrics={allMetrics.replicationcontroller} />

View File

@ -127,6 +127,8 @@ export const friendlyTitle = singularOrPluralResource => {
let titleCase = _startCase(resource);
if (resource === "replicationcontroller") {
titleCase = _startCase("replication controller");
} else if (resource === "daemonset") {
titleCase = _startCase("daemon set");
}
let titles = { singular: titleCase };
if (resource === "authority") {

View File

@ -62,6 +62,9 @@ let applicationHtml = (
<Route
path={`${pathPrefix}/namespaces/:namespace/pods/:pod`}
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
<Route
path={`${pathPrefix}/namespaces/:namespace/daemonsets/:daemonset`}
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
<Route
path={`${pathPrefix}/namespaces/:namespace/deployments/:deployment`}
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
@ -83,6 +86,9 @@ let applicationHtml = (
<Route
path={`${pathPrefix}/deployments`}
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="deployment" />} />
<Route
path={`${pathPrefix}/daemonsets`}
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="daemonset" />} />
<Route
path={`${pathPrefix}/replicationcontrollers`}
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="replicationcontroller" />} />

View File

@ -94,11 +94,13 @@ func NewServer(
server.router.GET("/servicemesh", handler.handleIndex)
server.router.GET("/namespaces", handler.handleIndex)
server.router.GET("/namespaces/:namespace", handler.handleIndex)
server.router.GET("/daemonsets", handler.handleIndex)
server.router.GET("/deployments", handler.handleIndex)
server.router.GET("/replicationcontrollers", handler.handleIndex)
server.router.GET("/pods", handler.handleIndex)
server.router.GET("/authorities", handler.handleIndex)
server.router.GET("/namespaces/:namespace/pods/:pod", handler.handleIndex)
server.router.GET("/namespaces/:namespace/daemonsets/:daemonset", handler.handleIndex)
server.router.GET("/namespaces/:namespace/deployments/:deployment", handler.handleIndex)
server.router.GET("/namespaces/:namespace/replicationcontrollers/:replicationcontroller", handler.handleIndex)
server.router.GET("/tap", handler.handleIndex)