From 121cf411f326fc93d600daa71faea1bb881487d2 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Thu, 30 Dec 2021 21:17:18 +0100 Subject: [PATCH] [applications] Improve topology graph It is now possible to apply custom styles in the topology graph. For this we added a new configuration option for the applications plugin named "topology". The "topology" configuration allows users to specify custom node types for the topology graph. For each node type the user must provide a "type", "shape" and "color". In the Application CRD we also added a new field "topology", where the subfield "type" can be used to select the node type for the application from the configuration. The "dependencies" field is now part of the "topology" field in the Applications CRD, which breaks compatibility with the former CRD for applications. The new styling works as follows: When a user sets a "topology.type" in the Application we use the corresponding node type from the applications plugin configuration to set the shape and color for the application in the topology graph. When an application is shown in the graph which is not within the selected namespaces or tags we also apply an opacity of 25%, to mark the application as "not selected". This is done by adding another node type for each user specified node type with the "-not-selected" suffix. When an application doesn't contain the "topology.type" field we use the default node type "application". To improve the applications plugin configuration we added also a new field "cache" under which the cache duration for the topology, teams and tags can be set. This is also an breaking change with the former plugin configuration. --- CHANGELOG.md | 1 + .../bookinfo/productpage-application.yaml | 11 ++-- deploy/demo/bookinfo/reviews-application.yaml | 7 +- .../elasticsearch-application.yaml | 15 +++-- .../elastic-system/filebeat-application.yaml | 9 +-- deploy/demo/kobs/base/kobs-application.yaml | 21 +++--- deploy/docker/kobs/config.yaml | 6 +- deploy/helm/kobs/Chart.yaml | 2 +- .../helm/kobs/crds/kobs.io_applications.yaml | 35 +++++----- .../kustomize/crds/kobs.io_applications.yaml | 35 +++++----- docs/plugins/applications.md | 34 ++++++++-- docs/resources/applications.md | 16 +++-- pkg/api/apis/application/v1beta1/types.go | 25 ++++--- .../v1beta1/zz_generated.deepcopy.go | 27 ++++++-- pkg/api/clusters/cluster/cluster.go | 8 +++ pkg/api/clusters/cluster/cluster_test.go | 9 ++- plugins/applications/applications.go | 29 +++++--- plugins/applications/applications_test.go | 37 ++++++----- plugins/applications/pkg/topology/topology.go | 66 ++++++++++++++++++- .../pkg/topology/topology_test.go | 52 ++++++++++----- .../src/components/page/Applications.tsx | 4 ++ .../applications/src/components/page/Page.tsx | 9 ++- .../components/panel/ApplicationsTopology.tsx | 10 ++- .../panel/ApplicationsTopologyGraph.tsx | 20 ++++-- .../src/components/panel/Panel.tsx | 25 +++++++ .../src/components/panel/details/Details.tsx | 26 ++++---- plugins/core/src/crds/application.ts | 7 +- .../src/components/dashboards/Dashboards.tsx | 4 +- plugins/dashboards/src/utils/dashboard.ts | 6 +- 29 files changed, 394 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4102069d..dee562830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#262](https://github.com/kobsio/kobs/pull/262): [core] Rework variables handling in dashboards. - [#263](https://github.com/kobsio/kobs/pull/263): [core] :warning: _Breaking change:_ :warning: Refactor `cluster` and `clusters` package. - [#265](https://github.com/kobsio/kobs/pull/265): [applications] Improve tags support by allow users to filter applications by tags and showing tags on application page. +- [#269](https://github.com/kobsio/kobs/pull/269): [applications] :warning: _Breaking change:_ :warning: Improve topology graph, by allowing custom styles for applications. ## [v0.7.0](https://github.com/kobsio/kobs/releases/tag/v0.7.0) (2021-11-19) diff --git a/deploy/demo/bookinfo/productpage-application.yaml b/deploy/demo/bookinfo/productpage-application.yaml index 8d695cec5..3c490d0d3 100644 --- a/deploy/demo/bookinfo/productpage-application.yaml +++ b/deploy/demo/bookinfo/productpage-application.yaml @@ -20,11 +20,12 @@ spec: namespace: kobs - name: team-call-of-duty namespace: kobs - dependencies: - - name: details - description: Get book information. - - name: reviews - description: Get book reviews. + topology: + dependencies: + - name: details + description: Get book information. + - name: reviews + description: Get book reviews. preview: title: Incoming Success Rate plugin: diff --git a/deploy/demo/bookinfo/reviews-application.yaml b/deploy/demo/bookinfo/reviews-application.yaml index 1d5f0443b..0b59d0240 100644 --- a/deploy/demo/bookinfo/reviews-application.yaml +++ b/deploy/demo/bookinfo/reviews-application.yaml @@ -16,9 +16,10 @@ spec: teams: - name: team-resident-evil namespace: kobs - dependencies: - - name: ratings - description: Get book ranking information. + topology: + dependencies: + - name: ratings + description: Get book ranking information. preview: title: Incoming Success Rate plugin: diff --git a/deploy/demo/elastic-system/elasticsearch-application.yaml b/deploy/demo/elastic-system/elasticsearch-application.yaml index ca0fa5e0e..2307f7584 100644 --- a/deploy/demo/elastic-system/elasticsearch-application.yaml +++ b/deploy/demo/elastic-system/elasticsearch-application.yaml @@ -12,13 +12,14 @@ spec: link: https://github.com/elastic/elasticsearch - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/elastic-system/elasticsearch-application.yaml - dependencies: - - name: elastic-operator - namespace: elastic-system - description: The Elasticsearch Cluster is managed by the Elastic Operator - - name: filebeat - namespace: elastic-system - description: Filebeat is responsible for sending the Logs of all Containers to Elasticsearch + topology: + dependencies: + - name: elastic-operator + namespace: elastic-system + description: The Elasticsearch Cluster is managed by the Elastic Operator + - name: filebeat + namespace: elastic-system + description: Filebeat is responsible for sending the Logs of all Containers to Elasticsearch preview: title: All Logs plugin: diff --git a/deploy/demo/elastic-system/filebeat-application.yaml b/deploy/demo/elastic-system/filebeat-application.yaml index 8151884c6..479ce8a2d 100644 --- a/deploy/demo/elastic-system/filebeat-application.yaml +++ b/deploy/demo/elastic-system/filebeat-application.yaml @@ -12,10 +12,11 @@ spec: link: https://github.com/elastic/beats - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/elastic-system/filebeat-application.yaml - dependencies: - - name: elastic-operator - namespace: elastic-system - description: Filebeat is managed by the Elastic Operator + topology: + dependencies: + - name: elastic-operator + namespace: elastic-system + description: Filebeat is managed by the Elastic Operator preview: title: All Logs plugin: diff --git a/deploy/demo/kobs/base/kobs-application.yaml b/deploy/demo/kobs/base/kobs-application.yaml index f0749c3ee..3c53237f1 100644 --- a/deploy/demo/kobs/base/kobs-application.yaml +++ b/deploy/demo/kobs/base/kobs-application.yaml @@ -13,16 +13,17 @@ spec: link: https://github.com/kobsio/kobs - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/kobs/kobs-application.yaml - dependencies: - - name: elasticsearch - namespace: elastic-system - description: Elasticsearch is used to get logs for the services. - - name: jaeger - namespace: istio-system - description: Jaeger is used to get traces for the services. - - name: prometheus - namespace: istio-system - description: Elasticsearch is used to get metrics for the services. + topology: + dependencies: + - name: elasticsearch + namespace: elastic-system + description: Elasticsearch is used to get logs for the services. + - name: jaeger + namespace: istio-system + description: Jaeger is used to get traces for the services. + - name: prometheus + namespace: istio-system + description: Elasticsearch is used to get metrics for the services. preview: title: All Logs plugin: diff --git a/deploy/docker/kobs/config.yaml b/deploy/docker/kobs/config.yaml index d5bfb9b93..7641acb01 100644 --- a/deploy/docker/kobs/config.yaml +++ b/deploy/docker/kobs/config.yaml @@ -10,8 +10,10 @@ plugins: - secrets applications: - topologyCacheDuration: 1m - teamsCacheDuration: 1m + cache: + topologyDuration: 1m + teamsDuration: 1m + tagsDuration: 1m prometheus: - name: prometheus diff --git a/deploy/helm/kobs/Chart.yaml b/deploy/helm/kobs/Chart.yaml index 5531a8903..dde4f9ecd 100644 --- a/deploy/helm/kobs/Chart.yaml +++ b/deploy/helm/kobs/Chart.yaml @@ -4,5 +4,5 @@ description: Kubernetes Observability Platform type: application home: https://kobs.io icon: https://kobs.io/assets/images/logo.svg -version: 0.8.4 +version: 0.9.0 appVersion: v0.7.0 diff --git a/deploy/helm/kobs/crds/kobs.io_applications.yaml b/deploy/helm/kobs/crds/kobs.io_applications.yaml index 1eaa3ee4f..1e95e1a88 100644 --- a/deploy/helm/kobs/crds/kobs.io_applications.yaml +++ b/deploy/helm/kobs/crds/kobs.io_applications.yaml @@ -123,21 +123,6 @@ spec: - title type: object type: array - dependencies: - items: - properties: - cluster: - type: string - description: - type: string - name: - type: string - namespace: - type: string - required: - - name - type: object - type: array description: type: string links: @@ -192,6 +177,26 @@ spec: - name type: object type: array + topology: + properties: + dependencies: + items: + properties: + cluster: + type: string + description: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + type: + type: string + type: object type: object type: object served: true diff --git a/deploy/kustomize/crds/kobs.io_applications.yaml b/deploy/kustomize/crds/kobs.io_applications.yaml index 1eaa3ee4f..1e95e1a88 100644 --- a/deploy/kustomize/crds/kobs.io_applications.yaml +++ b/deploy/kustomize/crds/kobs.io_applications.yaml @@ -123,21 +123,6 @@ spec: - title type: object type: array - dependencies: - items: - properties: - cluster: - type: string - description: - type: string - name: - type: string - namespace: - type: string - required: - - name - type: object - type: array description: type: string links: @@ -192,6 +177,26 @@ spec: - name type: object type: array + topology: + properties: + dependencies: + items: + properties: + cluster: + type: string + description: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + type: + type: string + type: object type: object type: object served: true diff --git a/docs/plugins/applications.md b/docs/plugins/applications.md index 9b9d3c635..df976dde1 100644 --- a/docs/plugins/applications.md +++ b/docs/plugins/applications.md @@ -4,20 +4,42 @@ The applications plugin allows you to show a list of application on a dashboard. ## Configuration -The following configuration can be used to configure the cache duration for applications. +The following configuration can be used to configure the cache duration for applications and to customize the topology graph. ```yaml plugins: applications: - topologyCacheDuration: 5m - teamsCacheDuration: 5m + cache: + topologyDuration: 5m + teamsDuration: 5m + tagsDuration: 5m + topology: + - type: custom + shape: round-diamond + color: "#c9190b" + ``` | Field | Type | Description | Required | | ----- | ---- | ----------- | -------- | -| topologyCacheDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the topology graph should be cached. The default value is `1h`. | No | -| teamsCacheDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the teams for an application should be cached. The default value is `1h`. | No | -| tagsCacheDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the tags for all applications should be cached. The default value is `1h`. | No | +| cache | [Cache](#cache) | Customize the caching behaviour for applications. | No | +| topology | [[]Topology](#topology) | Add custom node types for the topology graph, which can then selected in the Applications CRs via the `topology.type` option. | No | + +### Cache + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| topologyDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the topology graph should be cached. The default value is `1h`. | No | +| teamsDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the teams for an application should be cached. The default value is `1h`. | No | +| tagsDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the tags for all applications should be cached. The default value is `1h`. | No | + +### Topology + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| type | string | The name of the node type. The node type can be selected via the `topology.type` field in the Application CRs. | No | +| color | string | A color for the node in the topology chart. Every CSS color code is allowed, but we recommend to use an color from [Patternfly](https://www.patternfly.org/v4/guidelines/colors), so the topology graph matches the overall style of kobs. The default value is `#0066cc`. | No | +| shape | string | The shape of the node in the topology chart. Allowed values are `rectangle`, `roundrectangle`, `ellipse`, `triangle`, `pentagon`, `hexagon`, `heptagon`, `octagon`, `star`, `barrel`, `diamond`, `vee`, `rhomboid`, `polygon`, `tag`, `round-rectangle`, `round-triangle`, `round-diamond`, `round-pentagon`, `round-hexagon`, `round-heptagon`, `round-octagon`, `round-tag`, `cut-rectangle`, `bottom-round-rectangle` and `concave-hexagon`. The default value is `roundrectangle`. | No | ## Options diff --git a/docs/resources/applications.md b/docs/resources/applications.md index 415f2a89c..a14b7c2c2 100644 --- a/docs/resources/applications.md +++ b/docs/resources/applications.md @@ -22,7 +22,7 @@ In the following you can found the specification for the Application CRD. On the | tags | []string | A list of tags to describe the application. | No | | links | [[]Link](#link) | A list of links (e.g. a link to the GitHub repository for this application). | No | | teams | [[]Team](#team) | A list of teams to define the ownership for the application. | No | -| dependencies | [[]Dependency](#dependency) | Add other applications as dependencies for this application. This can be used to render a topology graph for your applications. | No | +| topology | [Topology](#topology) | Set the topology settings for your application like the type in the topology graph and dependencies to other applications. | No | | preview | [Preview](#preview) | Show the most important metrics for your application in the gallery view. | No | | dashboards | [[]Dashboard](#dashboard) | A list of dashboards, which should be shown for this application. | No | @@ -44,6 +44,13 @@ Teams can be used to define the ownership for an application. It is also possibl | name | string | Name of the team. | Yes | | description | string | The description can be used to explain, why this team is the owner of the application. | No | +### Topology + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| type | string | The type of the application in the topology graph. This must be a node type as specified in the `topology` key in the [configuration](../plugins/applications#configuration). The default value is `application`. | No | +| dependencies | [[]Dependency](#dependency) | Add other applications as dependencies for this application. This can be used to render a topology graph for your applications. | No | + ### Dependency Dependencies can be used to render a topology graph for all your applications. For that your have to add other applications as dependencies to the Application CR. @@ -110,9 +117,10 @@ spec: teams: - name: squad-resident-evil namespace: kobs - dependencies: - - name: ratings - description: Get book ranking information. + topology: + dependencies: + - name: ratings + description: Get book ranking information. preview: title: Incoming Success Rate plugin: diff --git a/pkg/api/apis/application/v1beta1/types.go b/pkg/api/apis/application/v1beta1/types.go index 14259baaa..e50942587 100644 --- a/pkg/api/apis/application/v1beta1/types.go +++ b/pkg/api/apis/application/v1beta1/types.go @@ -28,16 +28,16 @@ type ApplicationList struct { } type ApplicationSpec struct { - Cluster string `json:"cluster,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Tags []string `json:"tags,omitempty"` - Links []Link `json:"links,omitempty"` - Teams []Reference `json:"teams,omitempty"` - Dependencies []Reference `json:"dependencies,omitempty"` - Preview *Preview `json:"preview,omitempty"` - Dashboards []dashboard.Reference `json:"dashboards,omitempty"` + Cluster string `json:"cluster,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Links []Link `json:"links,omitempty"` + Teams []Reference `json:"teams,omitempty"` + Topology Topology `json:"topology,omitempty"` + Preview *Preview `json:"preview,omitempty"` + Dashboards []dashboard.Reference `json:"dashboards,omitempty"` } type Link struct { @@ -45,6 +45,11 @@ type Link struct { Link string `json:"link"` } +type Topology struct { + Type string `json:"type,omitempty"` + Dependencies []Reference `json:"dependencies,omitempty"` +} + type Reference struct { Cluster string `json:"cluster,omitempty"` Namespace string `json:"namespace,omitempty"` diff --git a/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go b/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go index d66021550..64d9d3372 100644 --- a/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go +++ b/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go @@ -104,11 +104,7 @@ func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { *out = make([]Reference, len(*in)) copy(*out, *in) } - if in.Dependencies != nil { - in, out := &in.Dependencies, &out.Dependencies - *out = make([]Reference, len(*in)) - copy(*out, *in) - } + in.Topology.DeepCopyInto(&out.Topology) if in.Preview != nil { in, out := &in.Preview, &out.Preview *out = new(Preview) @@ -182,3 +178,24 @@ func (in *Reference) DeepCopy() *Reference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Topology) DeepCopyInto(out *Topology) { + *out = *in + if in.Dependencies != nil { + in, out := &in.Dependencies, &out.Dependencies + *out = make([]Reference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Topology. +func (in *Topology) DeepCopy() *Topology { + if in == nil { + return nil + } + out := new(Topology) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/clusters/cluster/cluster.go b/pkg/api/clusters/cluster/cluster.go index 57a48ff4c..60292958a 100644 --- a/pkg/api/clusters/cluster/cluster.go +++ b/pkg/api/clusters/cluster/cluster.go @@ -391,6 +391,10 @@ func (c *client) GetApplications(ctx context.Context, namespace string) ([]appli application.Namespace = applicationItem.Namespace application.Name = applicationItem.Name + if application.Topology.Type == "" { + application.Topology.Type = "application" + } + applications = append(applications, application) } @@ -411,6 +415,10 @@ func (c *client) GetApplication(ctx context.Context, namespace, name string) (*a application.Namespace = namespace application.Name = name + if application.Topology.Type == "" { + application.Topology.Type = "application" + } + return &application, nil } diff --git a/pkg/api/clusters/cluster/cluster_test.go b/pkg/api/clusters/cluster/cluster_test.go index 732573eab..887e44e01 100644 --- a/pkg/api/clusters/cluster/cluster_test.go +++ b/pkg/api/clusters/cluster/cluster_test.go @@ -93,6 +93,11 @@ func TestGetApplications(t *testing.T) { Name: "application1", Namespace: "default", }, + Spec: application.ApplicationSpec{ + Topology: application.Topology{ + Type: "service", + }, + }, }, &application.Application{ ObjectMeta: metav1.ObjectMeta{ Name: "application2", @@ -117,7 +122,7 @@ func TestGetApplications(t *testing.T) { client := getClient() applications, err := client.GetApplications(context.Background(), "default") require.NoError(t, err) - require.Equal(t, []application.ApplicationSpec{{Cluster: "test", Namespace: "default", Name: "application1"}, {Cluster: "test", Namespace: "default", Name: "application2"}}, applications) + require.Equal(t, []application.ApplicationSpec{{Cluster: "test", Namespace: "default", Name: "application1", Topology: application.Topology{Type: "service"}}, {Cluster: "test", Namespace: "default", Name: "application2", Topology: application.Topology{Type: "application"}}}, applications) }) } @@ -153,7 +158,7 @@ func TestGetApplication(t *testing.T) { client := getClient() applications, err := client.GetApplication(context.Background(), "default", "application1") require.NoError(t, err) - require.Equal(t, &application.ApplicationSpec{Cluster: "test", Namespace: "default", Name: "application1"}, applications) + require.Equal(t, &application.ApplicationSpec{Cluster: "test", Namespace: "default", Name: "application1", Topology: application.Topology{Type: "application"}}, applications) }) } diff --git a/plugins/applications/applications.go b/plugins/applications/applications.go index b5837841f..cc14a60fa 100644 --- a/plugins/applications/applications.go +++ b/plugins/applications/applications.go @@ -24,9 +24,15 @@ const Route = "/applications" // Config is the structure of the configuration for the applications plugin. type Config struct { - TopologyCacheDuration string `json:"topologyCacheDuration"` - TeamsCacheDuration string `json:"teamsCacheDuration"` - TagsCacheDuration string `json:"tagsCacheDuration"` + Cache CacheConfig `json:"cache"` + Topology []topology.Config `json:"topology"` +} + +// CacheConfig is the structure of the cache configuration for the topology graph, teams and tags. +type CacheConfig struct { + TopologyDuration string `json:"topologyDuration"` + TeamsDuration string `json:"teamsDuration"` + TagsDuration string `json:"tagsDuration"` } // Router implements the router for the resources plugin, which can be registered in the router for our rest api. @@ -152,7 +158,7 @@ func (router *Router) getApplications(w http.ResponseWriter, r *http.Request) { // graph and generating a new one in the background. if view == "topology" { if router.topology.LastFetch.After(time.Now().Add(-1 * router.topology.CacheDuration)) { - topo := topology.Generate(router.topology.Topology, clusterNames, namespaces) + topo := topology.Generate(router.topology.Topology, clusterNames, namespaces, tagsList) log.Debug(r.Context(), "Get applications result.", zap.String("topology", "return cached topology"), zap.Int("edges", len(topo.Edges)), zap.Int("nodes", len(topo.Nodes))) render.JSON(w, r, topo) return @@ -164,7 +170,7 @@ func (router *Router) getApplications(w http.ResponseWriter, r *http.Request) { router.topology.LastFetch = time.Now() router.topology.Topology = topo - topo = topology.Generate(topo, clusterNames, namespaces) + topo = topology.Generate(topo, clusterNames, namespaces, tagsList) log.Debug(r.Context(), "Get applications result.", zap.String("topology", "get and return topology"), zap.Int("edges", len(topo.Edges)), zap.Int("nodes", len(topo.Nodes))) render.JSON(w, r, topo) return @@ -184,7 +190,7 @@ func (router *Router) getApplications(w http.ResponseWriter, r *http.Request) { } }() - topo := topology.Generate(router.topology.Topology, clusterNames, namespaces) + topo := topology.Generate(router.topology.Topology, clusterNames, namespaces, tagsList) log.Debug(r.Context(), "Get applications result.", zap.String("topology", "return topology"), zap.Int("edges", len(topo.Edges)), zap.Int("nodes", len(topo.Nodes))) render.JSON(w, r, topo) return @@ -259,16 +265,21 @@ func (router *Router) getTags(w http.ResponseWriter, r *http.Request) { // Register returns a new router which can be used in the router for the kobs rest api. func Register(clustersClient clusters.Client, plugins *plugin.Plugins, config Config) chi.Router { + var options map[string]interface{} + options = make(map[string]interface{}) + options["topology"] = config.Topology + plugins.Append(plugin.Plugin{ Name: "applications", DisplayName: "Applications", Description: "Monitor your Kubernetes workloads.", Home: true, Type: "applications", + Options: options, }) var topology topology.Cache - topologyCacheDuration, err := time.ParseDuration(config.TopologyCacheDuration) + topologyCacheDuration, err := time.ParseDuration(config.Cache.TopologyDuration) if err != nil || topologyCacheDuration.Seconds() < 60 { topology.CacheDuration = time.Duration(1 * time.Hour) } else { @@ -276,7 +287,7 @@ func Register(clustersClient clusters.Client, plugins *plugin.Plugins, config Co } var teams teams.Cache - teamsCacheDuration, err := time.ParseDuration(config.TeamsCacheDuration) + teamsCacheDuration, err := time.ParseDuration(config.Cache.TeamsDuration) if err != nil || teamsCacheDuration.Seconds() < 60 { teams.CacheDuration = time.Duration(1 * time.Hour) } else { @@ -284,7 +295,7 @@ func Register(clustersClient clusters.Client, plugins *plugin.Plugins, config Co } var tags tags.Cache - tagsCacheDuration, err := time.ParseDuration(config.TagsCacheDuration) + tagsCacheDuration, err := time.ParseDuration(config.Cache.TagsDuration) if err != nil || tagsCacheDuration.Seconds() < 60 { tags.CacheDuration = time.Duration(1 * time.Hour) } else { diff --git a/plugins/applications/applications_test.go b/plugins/applications/applications_test.go index a61f4fd39..8f487008c 100644 --- a/plugins/applications/applications_test.go +++ b/plugins/applications/applications_test.go @@ -56,7 +56,7 @@ func TestGetApplications(t *testing.T) { name: "gallery return teams with cached teams nil", url: "/applications?view=gallery&teamCluster=cluster1&teamNamespace=namespace1&teamName=team1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetTeams", mock.Anything, "").Return([]team.TeamSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}, nil) mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) @@ -77,7 +77,7 @@ func TestGetApplications(t *testing.T) { teamsCache: teams.Cache{Teams: []teams.Team{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1", Applications: []application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}}}}, url: "/applications?view=gallery&teamCluster=cluster1&teamNamespace=namespace1&teamName=team1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetTeams", mock.Anything, "").Return([]team.TeamSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}, nil) mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) @@ -106,7 +106,7 @@ func TestGetApplications(t *testing.T) { name: "gallery namespaces nil", url: "/applications?view=gallery&cluster=cluster1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) }, @@ -124,7 +124,7 @@ func TestGetApplications(t *testing.T) { name: "gallery", url: "/applications?view=gallery&cluster=cluster1&namespace=namespace1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "namespace1").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) }, @@ -144,12 +144,12 @@ func TestGetApplications(t *testing.T) { topologyCache: topology.Cache{Topology: &topology.Topology{Edges: nil, Nodes: nil}}, url: "/applications?view=topology&cluster=cluster1&namespace=namespace2", expectedStatusCode: http.StatusOK, - expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"application\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"application\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"application\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"application\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}}]}\n", + expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"-not-selected\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"topology\":{\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"topology\":{\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"-not-selected\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"-not-selected\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}}]}\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{ - {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}, - {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}, - {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}, + {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}, }, nil) @@ -175,21 +175,21 @@ func TestGetApplications(t *testing.T) { {Data: topology.EdgeData{ID: "cluster1-namespace2-application3-cluster2-namespace3-application5", Source: "cluster1-namespace2-application3", SourceCluster: "cluster1", SourceNamespace: "namespace2", SourceName: "application3", Target: "cluster2-namespace3-application5", TargetCluster: "cluster2", TargetNamespace: "namespace3", TargetName: "application5"}}, }, Nodes: []topology.Node{ - {Data: topology.NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}, - {Data: topology.NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}, - {Data: topology.NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}, + {Data: topology.NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}}, + {Data: topology.NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}}, + {Data: topology.NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}}, {Data: topology.NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}}}, {Data: topology.NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, }, }}, url: "/applications?view=topology&cluster=cluster1&namespace=namespace2", expectedStatusCode: http.StatusOK, - expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"application\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"application\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"application\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"application\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}}]}\n", + expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"application-not-selected\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"topology\":{\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"application\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"topology\":{\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"application-not-selected\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"application-not-selected\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}}]}\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{ - {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}, - {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}, - {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}, + {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}, }, nil) @@ -245,7 +245,7 @@ func TestGetApplication(t *testing.T) { name: "return application", url: "/application?cluster=cluster1&namespace=namespace1&name=application1", expectedStatusCode: http.StatusOK, - expectedBody: "{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\"}\n", + expectedBody: "{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"topology\":{}}\n", }, } { t.Run(tt.name, func(t *testing.T) { @@ -349,6 +349,9 @@ func TestRegister(t *testing.T) { Description: "Monitor your Kubernetes workloads.", Home: true, Type: "applications", + Options: map[string]interface{}{ + "topology": []topology.Config(nil), + }, }, } @@ -361,7 +364,7 @@ func TestRegister(t *testing.T) { t.Run("config with durations", func(t *testing.T) { plugins := &plugin.Plugins{} - router := Register(nil, plugins, Config{TopologyCacheDuration: "1m", TeamsCacheDuration: "1m", TagsCacheDuration: "1m"}) + router := Register(nil, plugins, Config{Cache: CacheConfig{TopologyDuration: "1m", TeamsDuration: "1m", TagsDuration: "1m"}}) require.NotEmpty(t, router) require.Equal(t, expectedPlugins, plugins) }) diff --git a/plugins/applications/pkg/topology/topology.go b/plugins/applications/pkg/topology/topology.go index fb12b2949..4832030f1 100644 --- a/plugins/applications/pkg/topology/topology.go +++ b/plugins/applications/pkg/topology/topology.go @@ -8,6 +8,15 @@ import ( "github.com/kobsio/kobs/pkg/api/clusters" ) +// Config is the structure of the topology graph configuration. It contains a type which can then be used in the +// "topology.type" property in an application. It also contains a user color and shape which defines how the application +// is rendered in the topology graph. +type Config struct { + Type string `json:"type"` + Shape string `json:"shape"` + Color string `json:"color"` +} + // Cache is the structure which can be used to cache the generated topology graph in the applications plugin. type Cache struct { LastFetch time.Time @@ -76,7 +85,7 @@ func Get(ctx context.Context, clustersClient clusters.Client) *Topology { nodes = append(nodes, Node{ Data: NodeData{ application.Cluster + "-" + application.Namespace + "-" + application.Name, - "application", + application.Topology.Type, application.Name, application.Cluster + "-" + application.Namespace, application, @@ -86,7 +95,7 @@ func Get(ctx context.Context, clustersClient clusters.Client) *Topology { // The cluster and namespace field in the reference for a dependency is optional. So that we have to set the // cluster and namespace to the cluster and namespace of the current application, when it isn't defined in // the dependency reference. - for _, dependency := range application.Dependencies { + for _, dependency := range application.Topology.Dependencies { dependencyCluster := dependency.Cluster if dependencyCluster == "" { dependencyCluster = application.Cluster @@ -139,7 +148,7 @@ func Get(ctx context.Context, clustersClient clusters.Client) *Topology { // are adding the source and target nodes. We are also creating an additional slice for the cluster and namespace nodes, // which are then merged with the nodes slice. This is necessary, because we do not save the clusters and namespaces as // nodes in the cached topology. -func Generate(topology *Topology, clusters, namespaces []string) *Topology { +func Generate(topology *Topology, clusters, namespaces, tags []string) *Topology { var edges []Edge var nodes []Node var clusterNodes []Node @@ -166,6 +175,7 @@ func Generate(topology *Topology, clusters, namespaces []string) *Topology { for _, edge := range edges { for _, node := range topology.Nodes { if node.Data.ID == edge.Data.Source || node.Data.ID == edge.Data.Target { + node.Data.Type = getNodeTyp(node, namespaces, tags) nodes = appendNodeIfMissing(nodes, node) clusterNode := Node{ @@ -232,3 +242,53 @@ func appendNodeIfMissing(nodes []Node, node Node) []Node { return append(nodes, node) } + +// getNodeTyp returns the correct node type for a node in the topology graph. This is required, because we only filter +// the topology graph by the selected namespaces and not tags. To apply a different style for node which are outside of +// the selected namespaces or for nodes which are not containing a tag. To change the style we add the "-not-selected" +// suffix. The following rules are applied: +// - No namespaces and tags were selected -- NORMAL +// - The node contains a selected tag --> NORMAL +// - The node is within a selected namespaces and the user didn't selected any tags --> NORMAL +// - Node is in the list of selected namespaces but not in the list of selected tags --> WITH SUFFIX +// - Node is not in the list of selected namespaces and tags --> SUFFIX +func getNodeTyp(node Node, namespaces, tags []string) string { + if namespaces == nil && tags == nil { + return node.Data.Type + } + + isInNamespace := isItemInItems(node.Data.Namespace, namespaces) + containsTag := isItemsInItems(node.Data.Tags, tags) + + if containsTag { + return node.Data.Type + } + + if tags == nil && isInNamespace { + return node.Data.Type + } + + return node.Data.Type + "-not-selected" +} + +// isItemsInItems return true, when one item from a list of actual items is in a list of expected items. +func isItemsInItems(itemsActual, itemExpected []string) bool { + for _, tag := range itemsActual { + if isItemInItems(tag, itemExpected) { + return true + } + } + + return false +} + +// isItemInItems returns true when the item is in a list of items. +func isItemInItems(item string, items []string) bool { + for _, i := range items { + if i == item { + return true + } + } + + return false +} diff --git a/plugins/applications/pkg/topology/topology_test.go b/plugins/applications/pkg/topology/topology_test.go index 9d2dcd4b3..617821b57 100644 --- a/plugins/applications/pkg/topology/topology_test.go +++ b/plugins/applications/pkg/topology/topology_test.go @@ -21,11 +21,11 @@ var expectedGetTopology = Topology{ {Data: EdgeData{ID: "cluster1-namespace2-application3-cluster2-namespace3-application5", Source: "cluster1-namespace2-application3", SourceCluster: "cluster1", SourceNamespace: "namespace2", SourceName: "application3", Target: "cluster2-namespace3-application5", TargetCluster: "cluster2", TargetNamespace: "namespace3", TargetName: "application5"}}, }, Nodes: []Node{ - {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}, - {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}, - {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}, - {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}}}, - {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4", Topology: application.Topology{Type: "application"}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5", Topology: application.Topology{Type: "application"}}}}, }, } @@ -37,11 +37,11 @@ var expectedGenerateTopology = Topology{ {Data: EdgeData{ID: "cluster1-namespace2-application3-cluster2-namespace3-application5", Source: "cluster1-namespace2-application3", SourceCluster: "cluster1", SourceNamespace: "namespace2", SourceName: "application3", Target: "cluster2-namespace3-application5", TargetCluster: "cluster2", TargetNamespace: "namespace3", TargetName: "application5"}}, }, Nodes: []Node{ - {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}, - {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}, - {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}, - {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}}}, - {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4", Topology: application.Topology{Type: "application"}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5", Topology: application.Topology{Type: "application"}}}}, {Data: NodeData{ID: "cluster1", Type: "cluster", Label: "cluster1", Parent: "", ApplicationSpec: application.ApplicationSpec{Cluster: "", Namespace: "", Name: ""}}}, {Data: NodeData{ID: "cluster2", Type: "cluster", Label: "cluster2", Parent: "", ApplicationSpec: application.ApplicationSpec{Cluster: "", Namespace: "", Name: ""}}}, {Data: NodeData{ID: "cluster1-namespace1", Type: "namespace", Label: "namespace1", Parent: "cluster1", ApplicationSpec: application.ApplicationSpec{Cluster: "", Namespace: "", Name: ""}}}, @@ -72,11 +72,11 @@ func TestGet(t *testing.T) { t.Run("get topology", func(t *testing.T) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{ - {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}, - {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}, - {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, - {Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, - {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}, + {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}, + {Cluster: "cluster2", Namespace: "namespace3", Name: "application4", Topology: application.Topology{Type: "application"}}, + {Cluster: "cluster2", Namespace: "namespace3", Name: "application5", Topology: application.Topology{Type: "application"}}, }, nil).Once() actualTopology := Get(context.Background(), mockClustersClient) require.Equal(t, &expectedGetTopology, actualTopology) @@ -84,10 +84,10 @@ func TestGet(t *testing.T) { } func TestGenerate(t *testing.T) { - actualTopology1 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, []string{"namespace1", "namespace2", "namespace3"}) + actualTopology1 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, []string{"namespace1", "namespace2", "namespace3"}, nil) require.Equal(t, &expectedGenerateTopology, actualTopology1) - actualTopology2 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, nil) + actualTopology2 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, nil, nil) require.Equal(t, &expectedGenerateTopology, actualTopology2) } @@ -139,3 +139,21 @@ func TestAppendNodeIfMissing(t *testing.T) { }) } } + +func TestGetNodeType(t *testing.T) { + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace1", Tags: []string{"tag1"}}}}, nil, nil)) + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace1", Tags: []string{"tag1"}}}}, nil, []string{"tag1", "tag2"})) + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace1", Tags: []string{"tag1"}}}}, []string{"namespace1", "namespace2"}, nil)) + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace3", Tags: []string{"tag1"}}}}, []string{"namespace1", "namespace2"}, []string{"tag1", "tag2"})) + require.Equal(t, "type1-not-selected", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace3", Tags: []string{"tag3"}}}}, []string{"namespace1", "namespace2"}, []string{"tag1", "tag2"})) +} + +func TestIsItemsInItems(t *testing.T) { + require.Equal(t, true, isItemsInItems([]string{"namespace2", "namespace3"}, []string{"namespace1", "namespace2", "namespace3"})) + require.Equal(t, false, isItemsInItems([]string{"namespace4", "namespace5"}, []string{"namespace1", "namespace2", "namespace3"})) +} + +func TestIsItemInItems(t *testing.T) { + require.Equal(t, true, isItemInItems("namespace1", []string{"namespace1", "namespace2", "namespace3"})) + require.Equal(t, false, isItemInItems("namespace4", []string{"namespace1", "namespace2", "namespace3"})) +} diff --git a/plugins/applications/src/components/page/Applications.tsx b/plugins/applications/src/components/page/Applications.tsx index 5faebf0f7..9cee412f2 100644 --- a/plugins/applications/src/components/page/Applications.tsx +++ b/plugins/applications/src/components/page/Applications.tsx @@ -13,6 +13,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import ApplicationsToolbar from './ApplicationsToolbar'; import { IOptions } from '../../utils/interfaces'; +import { IPluginDataOptions } from '@kobsio/plugin-core'; import Panel from '../panel/Panel'; import { getInitialOptions } from '../../utils/helpers'; @@ -20,6 +21,7 @@ export interface IApplicationsProps { name: string; displayName: string; description: string; + pluginOptions?: IPluginDataOptions; } // Applications is the page which lets the user query all the created applications by cluster and namespace. The user @@ -29,6 +31,7 @@ const Applications: React.FunctionComponent = ({ name, displayName, description, + pluginOptions, }: IApplicationsProps) => { const history = useHistory(); const location = useLocation(); @@ -92,6 +95,7 @@ const Applications: React.FunctionComponent = ({ view: options.view, }} times={options.times} + pluginOptions={pluginOptions} setDetails={setDetails} /> )} diff --git a/plugins/applications/src/components/page/Page.tsx b/plugins/applications/src/components/page/Page.tsx index 80d23dfb5..f32d981fc 100644 --- a/plugins/applications/src/components/page/Page.tsx +++ b/plugins/applications/src/components/page/Page.tsx @@ -8,11 +8,16 @@ import { IPluginPageProps } from '@kobsio/plugin-core'; // The page for the applications plugin, supports two different routes: One for showing a gallery / topology of // applications, where the user can select a list of clusters and namespaces for which he wants to get the applications // and a second one for showing a single application. -const Page: React.FunctionComponent = ({ name, displayName, description }: IPluginPageProps) => { +const Page: React.FunctionComponent = ({ + name, + displayName, + description, + pluginOptions, +}: IPluginPageProps) => { return ( - + diff --git a/plugins/applications/src/components/panel/ApplicationsTopology.tsx b/plugins/applications/src/components/panel/ApplicationsTopology.tsx index dda59d894..ee5665166 100644 --- a/plugins/applications/src/components/panel/ApplicationsTopology.tsx +++ b/plugins/applications/src/components/panel/ApplicationsTopology.tsx @@ -1,6 +1,7 @@ import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react-core'; import { QueryObserverResult, useQuery } from 'react-query'; import React, { memo } from 'react'; +import cytoscape from 'cytoscape'; import { useHistory } from 'react-router-dom'; import { IEdge, INode } from '../../utils/interfaces'; @@ -17,6 +18,7 @@ interface IApplicationsTopologyProps { namespaces: string[]; tags: string[]; times: IPluginTimes; + customStyleSheet: cytoscape.Stylesheet[]; setDetails?: (details: React.ReactNode) => void; } @@ -28,6 +30,7 @@ const ApplicationsTopology: React.FunctionComponent namespaces, tags, times, + customStyleSheet, setDetails, }: IApplicationsTopologyProps) => { const history = useHistory(); @@ -96,7 +99,12 @@ const ApplicationsTopology: React.FunctionComponent return (
- +
); }; diff --git a/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx b/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx index 1f397e90c..ea7ed040e 100644 --- a/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx +++ b/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx @@ -21,9 +21,9 @@ const layout = { rankDir: 'LR', }; -// styleSheet changes the style of the nodes and edges in the topology graph. +// defaultStyleSheet changes the style of the nodes and edges in the topology graph. // See: https://js.cytoscape.org/#style -const styleSheet: cytoscape.Stylesheet[] = [ +const defaultStyleSheet: cytoscape.Stylesheet[] = [ { selector: 'node', style: { @@ -44,6 +44,7 @@ const styleSheet: cytoscape.Stylesheet[] = [ selector: "node[type='cluster']", style: { 'background-color': '#f0f0f0', + 'border-style': 'dashed', }, }, { @@ -55,7 +56,16 @@ const styleSheet: cytoscape.Stylesheet[] = [ { selector: "node[type='application']", style: { - 'background-color': '#ffffff', + 'background-color': '#0066cc', + shape: 'roundrectangle', + }, + }, + { + selector: "node[type='application-not-selected']", + style: { + 'background-color': '#0066cc', + 'background-opacity': 0.25, + shape: 'roundrectangle', }, }, { @@ -92,6 +102,7 @@ const nodeLabel = (node: INodeData): string => { interface IApplicationsTopologyGraphProps { edges: IEdge[]; nodes: INode[]; + customStyleSheet: cytoscape.Stylesheet[]; setDetails?: (details: React.ReactNode) => void; } @@ -99,6 +110,7 @@ interface IApplicationsTopologyGraphProps { const ApplicationsTopologyGraph: React.FunctionComponent = ({ edges, nodes, + customStyleSheet, setDetails, }: IApplicationsTopologyGraphProps) => { const [width, setWidth] = useState(0); @@ -172,7 +184,7 @@ const ApplicationsTopologyGraph: React.FunctionComponent ) : null} diff --git a/plugins/applications/src/components/panel/Panel.tsx b/plugins/applications/src/components/panel/Panel.tsx index 064964066..f4ef66f1f 100644 --- a/plugins/applications/src/components/panel/Panel.tsx +++ b/plugins/applications/src/components/panel/Panel.tsx @@ -1,4 +1,5 @@ import React, { memo } from 'react'; +import cytoscape from 'cytoscape'; import { IPluginPanelProps, PluginCard, PluginOptionsMissing } from '@kobsio/plugin-core'; import ApplicationsGallery from './ApplicationsGallery'; @@ -19,6 +20,7 @@ export const Panel: React.FunctionComponent = ({ description, options, times, + pluginOptions, setDetails, }: IPanelProps) => { // We have to validate that the required options object was provided in the Application CR by a user. This is @@ -39,12 +41,35 @@ export const Panel: React.FunctionComponent = ({ // When a title is set we are sure that the component is used in a dashboard so we will wrap the topology component in // the PluginCard component. if (options.view === 'topology') { + const customStyleSheet: cytoscape.Stylesheet[] = []; + + if (pluginOptions && pluginOptions.topology && Array.isArray(pluginOptions.topology)) { + for (const topologyItem of pluginOptions.topology) { + customStyleSheet.push({ + selector: `node[type='${topologyItem.type}']`, + style: { + 'background-color': topologyItem.color || '#0066cc', + shape: topologyItem.shape || 'roundrectangle', + }, + }); + customStyleSheet.push({ + selector: `node[type='${topologyItem.type}-not-selected']`, + style: { + 'background-color': topologyItem.color || '#0066cc', + 'background-opacity': 0.25, + shape: topologyItem.shape || 'roundrectangle', + }, + }); + } + } + const topology = ( ); diff --git a/plugins/applications/src/components/panel/details/Details.tsx b/plugins/applications/src/components/panel/details/Details.tsx index ddf24776f..3a8762482 100644 --- a/plugins/applications/src/components/panel/details/Details.tsx +++ b/plugins/applications/src/components/panel/details/Details.tsx @@ -40,20 +40,20 @@ const Details: React.FunctionComponent = ({ application, close }:
- {application.tags && ( -

- {application.tags.map((tag) => ( - - {tag.toLowerCase()} - - ))} -

- )} {application.description &&

{application.description}

} - {(application.teams && application.teams.length > 0) || - (application.dependencies && application.dependencies.length > 0) || + + {(application.tags && application.tags.length > 0) || + (application.teams && application.teams.length > 0) || + (application.topology?.dependencies && application.topology.dependencies.length > 0) || (application.links && application.links.length > 0) ? ( + {application.tags && + application.tags.map((tag) => ( + + {tag.toLowerCase()} + + ))} + {application.teams && application.teams.length > 0 ? application.teams.map((team, index) => ( @@ -71,8 +71,8 @@ const Details: React.FunctionComponent = ({ application, close }: )) : null} - {application.dependencies && application.dependencies.length > 0 - ? application.dependencies.map((dependency, index) => ( + {application.topology?.dependencies && application.topology.dependencies.length > 0 + ? application.topology.dependencies.map((dependency, index) => ( = ({ // a drawer it can happen that we already show some dashboards in the main view and so we can not rely on the query // parameters. useEffect(() => { - if (setDetails !== undefined) { - setOptions(getInitialOptions(location.search, references, setDetails !== undefined)); - } + setOptions(getInitialOptions(location.search, references, setDetails !== undefined)); }, [location.search, references, setDetails]); // Fetch all dashboards. The dashboards are available via the data variable. To fetch the dashboards we have to pass diff --git a/plugins/dashboards/src/utils/dashboard.ts b/plugins/dashboards/src/utils/dashboard.ts index 3cb5da7f9..8f2e33ae6 100644 --- a/plugins/dashboards/src/utils/dashboard.ts +++ b/plugins/dashboards/src/utils/dashboard.ts @@ -31,9 +31,9 @@ export const rowHeight = (rowSize: number | undefined, rowSpan: number | undefin return `${rowSize * 150}px`; }; -// getOptionsFromSearch is used to parse the given search location and return is as options for Prometheus. This is -// needed, so that a user can explore his Prometheus data from a chart. When the user selects the explore action, we -// pass him to this page and pass the data via the URL parameters. +// getInitialOptions is used to parse the given search location and return is as options for Prometheus. This is needed, +// so that a user can explore his Prometheus data from a chart. When the user selects the explore action, we pass him to +// this page and pass the data via the URL parameters. export const getInitialOptions = ( search: string, references: IDashboardReference[],