Skip to content

Commit 05a143b

Browse files
authored
Improve Resource Handling (#46)
- Use kind and group for resource id, instead of the resource name and group, so that the id can be generated using the kind and apiVersion fields of a Kubernetes resource manifest. - Update name of `resource` variable to `resourceId` in frontend code, to make it more clear, when we are using the id and when the resource. - Link owner references in resource details to the corresponding resource explore query.
1 parent ee53f5e commit 05a143b

40 files changed

+426
-367
lines changed

pkg/kubernetes/cache.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// by its ID, and set all resources at once.
1111
type Cache interface {
1212
IsValid() bool
13-
GetKeysAndKinds() ([]string, []string)
13+
GetDataFrameValues() ([]string, []string, []string, []string, []string, []string)
1414
Get(id string) (Resource, bool)
1515
SetAll(resources map[string]Resource)
1616
}
@@ -33,17 +33,30 @@ func (c *cache) IsValid() bool {
3333
return true
3434
}
3535

36-
func (c *cache) GetKeysAndKinds() ([]string, []string) {
36+
func (c *cache) GetDataFrameValues() ([]string, []string, []string, []string, []string, []string) {
3737
c.lock.RLock()
3838
defer c.lock.RUnlock()
3939

40-
var keys []string
40+
var ids []string
4141
var kinds []string
42-
for k, v := range c.resources {
43-
keys = append(keys, k)
42+
var names []string
43+
var apiVersions []string
44+
var paths []string
45+
var namespaced []string
46+
47+
for _, v := range c.resources {
48+
ids = append(ids, v.ID)
4449
kinds = append(kinds, v.Kind)
50+
names = append(names, v.Name)
51+
apiVersions = append(apiVersions, v.APIVersion)
52+
paths = append(paths, v.Path)
53+
if v.Namespaced {
54+
namespaced = append(namespaced, "true")
55+
} else {
56+
namespaced = append(namespaced, "false")
57+
}
4558
}
46-
return keys, kinds
59+
return ids, kinds, apiVersions, names, paths, namespaced
4760
}
4861

4962
func (c *cache) Get(id string) (Resource, bool) {

pkg/kubernetes/client.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,17 @@ func (c *client) GetResourceIds(ctx context.Context) (*data.Frame, error) {
110110

111111
c.refreshCache(ctx)
112112

113-
// Get a list of all resource ids, which are all the keys in the cache and a
114-
// list of all kinds.
115-
keys, kinds := c.cache.GetKeysAndKinds()
113+
// Get the values for the data frame from the resource cache.
114+
ids, kinds, apiVersions, names, paths, namespaced := c.cache.GetDataFrameValues()
116115

117116
frame := data.NewFrame(
118117
"Resources",
119-
data.NewField("values", nil, keys),
118+
data.NewField("ids", nil, ids),
120119
data.NewField("kinds", nil, kinds),
120+
data.NewField("apiVersions", nil, apiVersions),
121+
data.NewField("names", nil, names),
122+
data.NewField("paths", nil, paths),
123+
data.NewField("namespaced", nil, namespaced),
121124
)
122125

123126
frame.SetMeta(&data.FrameMeta{
@@ -204,9 +207,9 @@ func (c *client) GetResources(ctx context.Context, user string, groups []string,
204207
for _, namespace := range namespaces {
205208
go func(namespace string) {
206209
defer resourcesWG.Done()
207-
c.logger.Debug("Getting resources", "resource", resource.Resource, "path", resource.Path, "namespace", namespace, "parameterName", parameterName, "parameterValue", parameterValue, "user", user)
210+
c.logger.Debug("Getting resources", "name", resource.Name, "path", resource.Path, "namespace", namespace, "parameterName", parameterName, "parameterValue", parameterValue, "user", user)
208211

209-
result, err := c.clientset.CoreV1().RESTClient().Get().AbsPath(resource.Path).Namespace(namespace).Resource(resource.Resource).Param(parameterName, parameterValue).SetHeader("Accept", "application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json").SetHeader("Impersonate-User", user).SetHeader("Impersonate-Group", groups...).DoRaw(ctx)
212+
result, err := c.clientset.CoreV1().RESTClient().Get().AbsPath(resource.Path).Namespace(namespace).Resource(resource.Name).Param(parameterName, parameterValue).SetHeader("Accept", "application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json").SetHeader("Impersonate-User", user).SetHeader("Impersonate-Group", groups...).DoRaw(ctx)
210213
if err != nil {
211214
c.logger.Error("Failed to get resources", "error", err.Error())
212215
span.RecordError(err)
@@ -285,7 +288,7 @@ func (c *client) getPodsAndContainers(ctx context.Context, user string, groups [
285288
c.refreshCache(ctx)
286289

287290
switch resourceId {
288-
case "pods":
291+
case "pod":
289292
resource, ok := c.cache.Get(resourceId)
290293
if !ok {
291294
err := fmt.Errorf("resource %s not found", resourceId)
@@ -294,7 +297,7 @@ func (c *client) getPodsAndContainers(ctx context.Context, user string, groups [
294297
return nil, nil, err
295298
}
296299

297-
result, err := c.clientset.CoreV1().RESTClient().Get().AbsPath(resource.Path).Namespace(namespace).Resource(resource.Resource).Name(name).SetHeader("Impersonate-User", user).SetHeader("Impersonate-Group", groups...).DoRaw(ctx)
300+
result, err := c.clientset.CoreV1().RESTClient().Get().AbsPath(resource.Path).Namespace(namespace).Resource(resource.Name).Name(name).SetHeader("Impersonate-User", user).SetHeader("Impersonate-Group", groups...).DoRaw(ctx)
298301
if err != nil {
299302
span.RecordError(err)
300303
span.SetStatus(codes.Error, err.Error())
@@ -327,7 +330,7 @@ func (c *client) getPodsAndContainers(ctx context.Context, user string, groups [
327330
return nil, nil, err
328331
}
329332

330-
result, err := c.clientset.CoreV1().RESTClient().Get().AbsPath(resource.Path).Namespace(namespace).Resource(resource.Resource).Name(name).SetHeader("Impersonate-User", user).SetHeader("Impersonate-Group", groups...).DoRaw(ctx)
333+
result, err := c.clientset.CoreV1().RESTClient().Get().AbsPath(resource.Path).Namespace(namespace).Resource(resource.Name).Name(name).SetHeader("Impersonate-User", user).SetHeader("Impersonate-Group", groups...).DoRaw(ctx)
331334
if err != nil {
332335
span.RecordError(err)
333336
span.SetStatus(codes.Error, err.Error())

pkg/kubernetes/client_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func TestGetResources(t *testing.T) {
114114
require.NoError(t, err)
115115

116116
t.Run("should return resources nodes data frame", func(t *testing.T) {
117-
actualFrame, err := client.GetResources(context.Background(), "", nil, "nodes", "*", "", "", true)
117+
actualFrame, err := client.GetResources(context.Background(), "", nil, "node", "*", "", "", true)
118118
require.NoError(t, err)
119119
require.Equal(t, "Name", actualFrame.Fields[0].Name)
120120
require.Equal(t, "Status", actualFrame.Fields[1].Name)
@@ -130,7 +130,7 @@ func TestGetResources(t *testing.T) {
130130
})
131131

132132
t.Run("should return resources pods data frame", func(t *testing.T) {
133-
actualFrame, err := client.GetResources(context.Background(), "", nil, "pods", "default", "", "", false)
133+
actualFrame, err := client.GetResources(context.Background(), "", nil, "pod", "default", "", "", false)
134134
require.NoError(t, err)
135135
require.Equal(t, "Namespace", actualFrame.Fields[0].Name)
136136
require.Equal(t, "Name", actualFrame.Fields[1].Name)
@@ -157,7 +157,7 @@ func TestGetContainers(t *testing.T) {
157157
Type: data.FrameTypeTable,
158158
})
159159

160-
actualFrame, err := client.GetContainers(context.Background(), "", nil, "deployments.apps", "default", "echoserver")
160+
actualFrame, err := client.GetContainers(context.Background(), "", nil, "deployment.apps", "default", "echoserver")
161161
require.NoError(t, err)
162162
require.Equal(t, expectedFrame, actualFrame)
163163
})
@@ -169,7 +169,7 @@ func TestGetLogs(t *testing.T) {
169169
require.NoError(t, err)
170170

171171
t.Run("should return logs", func(t *testing.T) {
172-
actualLogs, err := client.GetLogs(context.Background(), "", nil, "pods", "default", "echoserver", "echoserver", "", backend.TimeRange{From: time.Now().Add(-1 * time.Hour), To: time.Now().Add(1 * time.Hour)})
172+
actualLogs, err := client.GetLogs(context.Background(), "", nil, "pod", "default", "echoserver", "echoserver", "", backend.TimeRange{From: time.Now().Add(-1 * time.Hour), To: time.Now().Add(1 * time.Hour)})
173173
require.NoError(t, err)
174174
require.Equal(t, "timestamp", actualLogs.Fields[0].Name)
175175
require.Equal(t, "body", actualLogs.Fields[1].Name)
@@ -187,7 +187,7 @@ func TestGetLogs(t *testing.T) {
187187
})
188188

189189
t.Run("should return filtered logs", func(t *testing.T) {
190-
actualLogs, err := client.GetLogs(context.Background(), "", nil, "pods", "default", "echoserver", "echoserver", "build", backend.TimeRange{From: time.Now().Add(-1 * time.Hour), To: time.Now().Add(1 * time.Hour)})
190+
actualLogs, err := client.GetLogs(context.Background(), "", nil, "pod", "default", "echoserver", "echoserver", "build", backend.TimeRange{From: time.Now().Add(-1 * time.Hour), To: time.Now().Add(1 * time.Hour)})
191191
require.NoError(t, err)
192192
require.Equal(t, "timestamp", actualLogs.Fields[0].Name)
193193
require.Equal(t, "body", actualLogs.Fields[1].Name)
@@ -212,9 +212,9 @@ func TestGetResource(t *testing.T) {
212212
require.NoError(t, err)
213213

214214
t.Run("should return resource", func(t *testing.T) {
215-
actualResource, err := client.GetResource(context.Background(), "deployments.apps")
215+
actualResource, err := client.GetResource(context.Background(), "deployment.apps")
216216
require.NoError(t, err)
217-
require.Equal(t, &Resource{Kind: "Deployment", Resource: "deployments", Path: "/apis/apps/v1", Namespaced: true}, actualResource)
217+
require.Equal(t, &Resource{ID: "deployment.apps", Kind: "Deployment", APIVersion: "apps/v1", Name: "deployments", Path: "/apis/apps/v1", Namespaced: true}, actualResource)
218218
})
219219
}
220220

pkg/kubernetes/resources.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,29 @@ func (c *client) getResources(ctx context.Context) (map[string]Resource, error)
3838
pathPrefix = "/api"
3939
}
4040

41-
id := resource.Name
41+
// Create a unique id for the resource based on its kind and group.
42+
// If the resource has no group, we just use the kind as id. We do
43+
// not include the version in the id, because the version can change
44+
// and might break existing references in dashboards.
45+
//
46+
// The frontend implementation of generating the id, could be found
47+
// in the "utils.resource.ts" file ("getResourceId" function).
48+
id := resource.Kind
4249
groupVersion := strings.Split(list.GroupVersion, "/")
4350
if len(groupVersion) == 2 {
44-
id = fmt.Sprintf("%s.%s", resource.Name, groupVersion[0])
51+
id = fmt.Sprintf("%s.%s", resource.Kind, groupVersion[0])
4552
}
53+
id = strings.ToLower(id)
4654

4755
if slices.Contains(resource.Verbs, "list") {
48-
resources[id] = Resource{Kind: resource.Kind, Resource: resource.Name, Path: fmt.Sprintf("%s/%s", pathPrefix, list.GroupVersion), Namespaced: resource.Namespaced}
56+
resources[id] = Resource{
57+
ID: id,
58+
Kind: resource.Kind,
59+
APIVersion: list.GroupVersion,
60+
Name: resource.Name,
61+
Path: fmt.Sprintf("%s/%s", pathPrefix, list.GroupVersion),
62+
Namespaced: resource.Namespaced,
63+
}
4964
}
5065
}
5166
}
@@ -117,35 +132,27 @@ func createResourcesDataFrame(resourceId string, resources [][]byte, namespaced,
117132
}
118133

119134
func formatColumnName(resourceId, name string) string {
120-
if resourceId == "pods.metrics.k8s.io" && name == "cpu" {
135+
if (resourceId == "podmetrics.metrics.k8s.io" || resourceId == "nodemetrics.metrics.k8s.io") && name == "cpu" {
121136
return "CPU"
122137
}
123138

124-
if resourceId == "pods.metrics.k8s.io" && name == "memory" {
125-
return "Memory"
126-
}
127-
128-
if resourceId == "nodes.metrics.k8s.io" && name == "cpu" {
129-
return "CPU"
130-
}
131-
132-
if resourceId == "nodes.metrics.k8s.io" && name == "memory" {
139+
if (resourceId == "podmetrics.metrics.k8s.io" || resourceId == "nodemetrics.metrics.k8s.io") && name == "memory" {
133140
return "Memory"
134141
}
135142

136143
return name
137144
}
138145

139146
func formatValue(resourceId, name string, value any) string {
140-
if (resourceId == "pods.metrics.k8s.io" || resourceId == "nodes.metrics.k8s.io") && name == "cpu" {
147+
if (resourceId == "podmetrics.metrics.k8s.io" || resourceId == "nodemetrics.metrics.k8s.io") && name == "cpu" {
141148
quantity, err := resource.ParseQuantity(fmt.Sprintf("%v", value))
142149
if err != nil {
143150
return fmt.Sprintf("%v", value)
144151
}
145152
return fmt.Sprintf("%vm", quantity.MilliValue())
146153
}
147154

148-
if (resourceId == "pods.metrics.k8s.io" || resourceId == "nodes.metrics.k8s.io") && name == "memory" {
155+
if (resourceId == "podmetrics.metrics.k8s.io" || resourceId == "nodemetrics.metrics.k8s.io") && name == "memory" {
149156
quantity, err := resource.ParseQuantity(fmt.Sprintf("%v", value))
150157
if err != nil {
151158
return fmt.Sprintf("%v", value)

pkg/kubernetes/types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
// be able to fetch all resource dynamically from the Kubernetes API, based on
1212
// the "Path" and "Resource" fields.
1313
type Resource struct {
14+
ID string `json:"id"`
1415
Kind string `json:"kind"`
15-
Resource string `json:"resource"`
16+
APIVersion string `json:"apiVersion"`
17+
Name string `json:"name"`
1618
Path string `json:"path"`
1719
Namespaced bool `json:"namespaced"`
1820
}

pkg/models/query.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,25 @@ const (
1414
)
1515

1616
type QueryModelKubernetesResources struct {
17-
Resource string `json:"resource"`
17+
ResourceId string `json:"resourceId"`
1818
Namespace string `json:"namespace"`
1919
ParameterName string `json:"parameterName"`
2020
ParameterValue string `json:"parameterValue"`
2121
Wide bool `json:"wide"`
2222
}
2323

2424
type QueryModelKubernetesContainers struct {
25-
Resource string `json:"resource"`
26-
Namespace string `json:"namespace"`
27-
Name string `json:"name"`
25+
ResourceId string `json:"resourceId"`
26+
Namespace string `json:"namespace"`
27+
Name string `json:"name"`
2828
}
2929

3030
type QueryModelKubernetesLogs struct {
31-
Resource string `json:"resource"`
32-
Namespace string `json:"namespace"`
33-
Name string `json:"name"`
34-
Container string `json:"container"`
35-
Filter string `json:"filter"`
31+
ResourceId string `json:"resourceId"`
32+
Namespace string `json:"namespace"`
33+
Name string `json:"name"`
34+
Container string `json:"container"`
35+
Filter string `json:"filter"`
3636
}
3737

3838
type QueryModelHelmReleases struct {

pkg/plugin/kubernetes.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,16 @@ func (d *Datasource) handleKubernetesResources(ctx context.Context, query concur
108108
return backend.ErrorResponseWithErrorSource(err)
109109
}
110110

111-
d.logger.Info("handleKubernetesResources query", "user", user, "groups", groups, "resource", qm.Resource, "namespace", qm.Namespace, "parameterName", qm.ParameterName, "parameterValue", qm.ParameterValue, "wide", qm.Wide)
111+
d.logger.Info("handleKubernetesResources query", "user", user, "groups", groups, "resourceId", qm.ResourceId, "namespace", qm.Namespace, "parameterName", qm.ParameterName, "parameterValue", qm.ParameterValue, "wide", qm.Wide)
112112
span.SetAttributes(attribute.Key("user").String(user))
113113
span.SetAttributes(attribute.Key("groups").StringSlice(groups))
114-
span.SetAttributes(attribute.Key("resource").String(qm.Resource))
114+
span.SetAttributes(attribute.Key("resourceId").String(qm.ResourceId))
115115
span.SetAttributes(attribute.Key("namespace").String(qm.Namespace))
116116
span.SetAttributes(attribute.Key("parameterName").String(qm.ParameterName))
117117
span.SetAttributes(attribute.Key("parameterValue").String(qm.ParameterValue))
118118
span.SetAttributes(attribute.Key("wide").Bool(qm.Wide))
119119

120-
frame, err := d.kubeClient.GetResources(ctx, user, groups, qm.Resource, qm.Namespace, qm.ParameterName, qm.ParameterValue, qm.Wide)
120+
frame, err := d.kubeClient.GetResources(ctx, user, groups, qm.ResourceId, qm.Namespace, qm.ParameterName, qm.ParameterValue, qm.Wide)
121121
if err != nil {
122122
d.logger.Error("Failed to get resources", "error", err.Error())
123123
span.RecordError(err)
@@ -170,14 +170,14 @@ func (d *Datasource) handleKubernetesContainers(ctx context.Context, query concu
170170
return backend.ErrorResponseWithErrorSource(err)
171171
}
172172

173-
d.logger.Info("handleKubernetesContainers query", "user", user, "groups", groups, "resource", qm.Resource, "namespace", qm.Namespace, "name", qm.Name)
173+
d.logger.Info("handleKubernetesContainers query", "user", user, "groups", groups, "resourceId", qm.ResourceId, "namespace", qm.Namespace, "name", qm.Name)
174174
span.SetAttributes(attribute.Key("user").String(user))
175175
span.SetAttributes(attribute.Key("groups").StringSlice(groups))
176-
span.SetAttributes(attribute.Key("resource").String(qm.Resource))
176+
span.SetAttributes(attribute.Key("resourceId").String(qm.ResourceId))
177177
span.SetAttributes(attribute.Key("namespace").String(qm.Namespace))
178178
span.SetAttributes(attribute.Key("name").String(qm.Name))
179179

180-
frame, err := d.kubeClient.GetContainers(ctx, user, groups, qm.Resource, qm.Namespace, qm.Name)
180+
frame, err := d.kubeClient.GetContainers(ctx, user, groups, qm.ResourceId, qm.Namespace, qm.Name)
181181
if err != nil {
182182
d.logger.Error("Failed to get containers", "error", err.Error())
183183
span.RecordError(err)
@@ -229,16 +229,16 @@ func (d *Datasource) handleKubernetesLogs(ctx context.Context, query concurrent.
229229
return backend.ErrorResponseWithErrorSource(err)
230230
}
231231

232-
d.logger.Info("handleKubernetesLogs query", "user", user, "groups", groups, "resource", qm.Resource, "namespace", qm.Namespace, "name", qm.Name, "container", qm.Container, "filter", qm.Filter)
232+
d.logger.Info("handleKubernetesLogs query", "user", user, "groups", groups, "resourceId", qm.ResourceId, "namespace", qm.Namespace, "name", qm.Name, "container", qm.Container, "filter", qm.Filter)
233233
span.SetAttributes(attribute.Key("user").String(user))
234234
span.SetAttributes(attribute.Key("groups").StringSlice(groups))
235-
span.SetAttributes(attribute.Key("resource").String(qm.Resource))
235+
span.SetAttributes(attribute.Key("resourceId").String(qm.ResourceId))
236236
span.SetAttributes(attribute.Key("namespace").String(qm.Namespace))
237237
span.SetAttributes(attribute.Key("name").String(qm.Name))
238238
span.SetAttributes(attribute.Key("container").String(qm.Container))
239239
span.SetAttributes(attribute.Key("filter").String(qm.Filter))
240240

241-
frame, err := d.kubeClient.GetLogs(ctx, user, groups, qm.Resource, qm.Namespace, qm.Name, qm.Container, qm.Filter, query.DataQuery.TimeRange)
241+
frame, err := d.kubeClient.GetLogs(ctx, user, groups, qm.ResourceId, qm.Namespace, qm.Name, qm.Container, qm.Filter, query.DataQuery.TimeRange)
242242
if err != nil {
243243
d.logger.Error("Failed to get logs", "error", err.Error())
244244
span.RecordError(err)

provisioning/dashboards/kubernetes/kubernetes-workloads.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
"allowCustomValue": false,
145145
"current": {
146146
"text": "Deployment",
147-
"value": "deployments.apps"
147+
"value": "deployment.apps"
148148
},
149149
"description": "",
150150
"label": "Resource",
@@ -153,30 +153,30 @@
153153
{
154154
"selected": false,
155155
"text": "DaemonSet",
156-
"value": "daemonsets.apps"
156+
"value": "daemonset.apps"
157157
},
158158
{
159159
"selected": true,
160160
"text": "Deployment",
161-
"value": "deployments.apps"
161+
"value": "deployment.apps"
162162
},
163163
{
164164
"selected": false,
165165
"text": "Pod",
166-
"value": "pods"
166+
"value": "pod"
167167
},
168168
{
169169
"selected": false,
170170
"text": "Job",
171-
"value": "jobs.batch"
171+
"value": "job.batch"
172172
},
173173
{
174174
"selected": false,
175175
"text": "StatefulSet",
176-
"value": "statefulsets.apps"
176+
"value": "statefulset.apps"
177177
}
178178
],
179-
"query": "DaemonSet : daemonsets.apps, Deployment : deployments.apps, Pod : pods, Job : jobs.batch, StatefulSet : statefulsets.apps",
179+
"query": "DaemonSet : daemonset.apps, Deployment : deployment.apps, Pod : pod, Job : job.batch, StatefulSet : statefulset.apps",
180180
"type": "custom"
181181
},
182182
{

0 commit comments

Comments
 (0)