Skip to content

Commit 8fe2d86

Browse files
committed
metrics: implement test for the metrics generator
1 parent a87fd4d commit 8fe2d86

8 files changed

+475
-0
lines changed
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package metrics
17+
18+
import (
19+
"bytes"
20+
"fmt"
21+
"io"
22+
"os"
23+
"path"
24+
"strings"
25+
"testing"
26+
27+
"github.com/google/go-cmp/cmp"
28+
"sigs.k8s.io/controller-tools/pkg/genall"
29+
"sigs.k8s.io/controller-tools/pkg/loader"
30+
"sigs.k8s.io/controller-tools/pkg/markers"
31+
"sigs.k8s.io/controller-tools/pkg/metrics/internal/config"
32+
)
33+
34+
func Test_Generate(t *testing.T) {
35+
cwd, err := os.Getwd()
36+
if err != nil {
37+
t.Error(err)
38+
}
39+
40+
optionsRegistry := &markers.Registry{}
41+
42+
metricGenerator := Generator{}
43+
if err := metricGenerator.RegisterMarkers(optionsRegistry); err != nil {
44+
t.Error(err)
45+
}
46+
47+
out := &outputRule{
48+
buf: &bytes.Buffer{},
49+
}
50+
51+
// Load the passed packages as roots.
52+
roots, err := loader.LoadRoots(path.Join(cwd, "testdata", "..."))
53+
if err != nil {
54+
t.Errorf("loading packages %v", err)
55+
}
56+
57+
gen := Generator{}
58+
59+
generationContext := &genall.GenerationContext{
60+
Collector: &markers.Collector{Registry: optionsRegistry},
61+
Roots: roots,
62+
Checker: &loader.TypeChecker{},
63+
OutputRule: out,
64+
}
65+
66+
t.Log("Trying to generate a custom resource configuration from the loaded packages")
67+
68+
if err := gen.Generate(generationContext); err != nil {
69+
t.Error(err)
70+
}
71+
72+
output := strings.Split(out.buf.String(), "\n---\n")
73+
74+
header := fmt.Sprintf(headerText, "(devel)", config.KubeStateMetricsVersion)
75+
76+
if len(output) != 3 {
77+
t.Error("Expected two output files, metrics configuration followed by rbac.")
78+
return
79+
}
80+
81+
generatedData := map[string]string{
82+
"metrics.yaml": header + "---\n" + string(output[1]),
83+
"rbac.yaml": "---\n" + string(output[2]),
84+
}
85+
86+
t.Log("Comparing output to testdata to check for regressions")
87+
88+
for _, golden := range []string{"metrics.yaml", "rbac.yaml"} {
89+
// generatedRaw := strings.TrimSpace(output[i])
90+
91+
expectedRaw, err := os.ReadFile(path.Clean(path.Join(cwd, "testdata", golden)))
92+
if err != nil {
93+
t.Error(err)
94+
return
95+
}
96+
97+
// Remove leading `---` and trim newlines
98+
generated := strings.TrimSpace(strings.TrimPrefix(generatedData[golden], "---"))
99+
expected := strings.TrimSpace(strings.TrimPrefix(string(expectedRaw), "---"))
100+
101+
diff := cmp.Diff(expected, generated)
102+
if diff != "" {
103+
t.Log("generated:")
104+
t.Log(generated)
105+
t.Log("diff:")
106+
t.Log(diff)
107+
t.Logf("Expected output to match file `testdata/%s` but it does not.", golden)
108+
t.Logf("If the change is intended, use `go generate ./pkg/metrics/testdata` to regenerate the `testdata/%s` file.", golden)
109+
t.Errorf("Detected a diff between the output of the integration test and the file `testdata/%s`.", golden)
110+
return
111+
}
112+
}
113+
}
114+
115+
type outputRule struct {
116+
buf *bytes.Buffer
117+
}
118+
119+
func (o *outputRule) Open(_ *loader.Package, _ string) (io.WriteCloser, error) {
120+
return nopCloser{o.buf}, nil
121+
}
122+
123+
type nopCloser struct {
124+
io.Writer
125+
}
126+
127+
func (n nopCloser) Close() error {
128+
return nil
129+
}

pkg/metrics/testdata/README.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Testdata for generator tests
2+
3+
The files in this directory are used for testing the `kube-state-metrics generate` command and to provide an example.
4+
5+
## foo-config.yaml
6+
7+
This file is used in the test at [generate_integration_test.go](../generate_integration_test.go) to verify that the resulting configuration does not change during changes in the codebase.
8+
9+
If there are intended changes this file needs to get regenerated to make the test succeed again.
10+
This could be done via:
11+
12+
```sh
13+
go run ./cmd/controller-gen metrics crd \
14+
paths=./pkg/metrics/testdata \
15+
output:dir=./pkg/metrics/testdata
16+
```
17+
18+
Or by using the go:generate marker inside [foo_types.go](foo_types.go):
19+
20+
```sh
21+
go generate ./pkg/metrics/testdata/
22+
```
23+
24+
## Example files: metrics.yaml, rbac.yaml and example-metrics.txt
25+
26+
There is also an example CR ([example-foo.yaml](example-foo.yaml)) and resulting example metrics ([example-metrics.txt](example-metrics.txt)).
27+
28+
The example metrics file got created by:
29+
30+
1. Generating a CustomResourceDefinition and Kube-State-Metrics configration file:
31+
32+
```sh
33+
go generate ./pkg/metrics/testdata/
34+
```
35+
36+
2. Creating a cluster using [kind](https://kind.sigs.k8s.io/)
37+
38+
```sh
39+
kind create cluster
40+
```
41+
42+
3. Applying the CRD and example CR to the cluster:
43+
44+
```sh
45+
kubectl apply -f ./pkg/metrics/testdata/bar.example.com_foos.yaml
46+
kubectl apply -f ./pkg/metrics/testdata/example-foo.yaml
47+
```
48+
49+
4. Running kube-state-metrics with the provided configuration file:
50+
51+
```sh
52+
docker run --net=host -ti --rm \
53+
-v $HOME/.kube/config:/config \
54+
-v $(pwd):/data \
55+
registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.13.0 \
56+
--kubeconfig /config --custom-resource-state-only \
57+
--custom-resource-state-config-file /data/pkg/metrics/testdata/foo-config.yaml
58+
```
59+
60+
5. Querying the metrics endpoint in a second terminal:
61+
62+
```sh
63+
curl localhost:8080/metrics > ./pkg/metrics/testdata/foo-cr-example-metrics.txt
64+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
controller-gen.kubebuilder.io/version: (devel)
7+
name: foos.bar.example.com
8+
spec:
9+
group: bar.example.com
10+
names:
11+
kind: Foo
12+
listKind: FooList
13+
plural: foos
14+
singular: foo
15+
scope: Namespaced
16+
versions:
17+
- name: foo
18+
schema:
19+
openAPIV3Schema:
20+
description: Foo is a test object.
21+
properties:
22+
apiVersion:
23+
description: |-
24+
APIVersion defines the versioned schema of this representation of an object.
25+
Servers should convert recognized schemas to the latest internal value, and
26+
may reject unrecognized values.
27+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
28+
type: string
29+
kind:
30+
description: |-
31+
Kind is a string value representing the REST resource this object represents.
32+
Servers may infer this from the endpoint the client submits requests to.
33+
Cannot be updated.
34+
In CamelCase.
35+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
36+
type: string
37+
metadata:
38+
type: object
39+
spec:
40+
description: Spec comments SHOULD appear in the CRD spec
41+
properties:
42+
someString:
43+
description: SomeString is a string.
44+
type: string
45+
required:
46+
- someString
47+
type: object
48+
status:
49+
description: Status comments SHOULD appear in the CRD spec
50+
properties:
51+
conditions:
52+
items:
53+
description: Condition is a test condition.
54+
properties:
55+
lastTransitionTime:
56+
description: LastTransitionTime of condition.
57+
format: date-time
58+
type: string
59+
status:
60+
description: Status of condition.
61+
type: string
62+
type:
63+
description: Type of condition.
64+
type: string
65+
required:
66+
- lastTransitionTime
67+
- status
68+
- type
69+
type: object
70+
type: array
71+
type: object
72+
type: object
73+
served: true
74+
storage: true

pkg/metrics/testdata/example-foo.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
apiVersion: bar.example.com/foo
3+
kind: Foo
4+
metadata:
5+
name: bar
6+
ownerReferences:
7+
- apiVersion: v1
8+
kind: foo
9+
controller: true
10+
name: foo
11+
uid: someuid
12+
spec:
13+
someString: test
14+
status:
15+
conditions:
16+
- lastTransitionTime: "2023-10-12T13:59:02Z"
17+
status: "True"
18+
type: SomeType
19+
- lastTransitionTime: "2023-10-12T13:59:02Z"
20+
status: "False"
21+
type: AnotherType
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# HELP foo_created Unix creation timestamp.
2+
# TYPE foo_created gauge
3+
foo_created{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar"} 1.724940463e+09
4+
# HELP foo_owner Owner references.
5+
# TYPE foo_owner info
6+
foo_owner{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",owner_is_controller="true",owner_kind="foo",owner_name="foo",owner_uid="someuid"} 1
7+
# HELP foo_status_condition The condition of a foo.
8+
# TYPE foo_status_condition stateset
9+
foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="False",type="AnotherType"} 1
10+
foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="False",type="SomeType"} 0
11+
foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="True",type="AnotherType"} 0
12+
foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="True",type="SomeType"} 1
13+
foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="Unknown",type="AnotherType"} 0
14+
foo_status_condition{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="Unknown",type="SomeType"} 0
15+
# HELP foo_status_condition_last_transition_time The condition last transition time of a foo.
16+
# TYPE foo_status_condition_last_transition_time gauge
17+
foo_status_condition_last_transition_time{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="False",type="AnotherType"} 1.697119142e+09
18+
foo_status_condition_last_transition_time{customresource_group="bar.example.com",customresource_kind="Foo",customresource_version="foo",name="bar",status="True",type="SomeType"} 1.697119142e+09

pkg/metrics/testdata/foo_types.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Changes to this file may require to regenerate the `foo-config.yaml`. Otherwise the
18+
// tests in ../generate_integration_test.go may fail.
19+
// The below marker can be used to regenerate the `foo-config.yaml` file by running
20+
// the following command:
21+
// $ go generate ./pkg/customresourcestate/generate/generator/testdata
22+
//go:generate sh -c "go run ../../../cmd/controller-gen crd metrics paths=./ output:dir=."
23+
24+
// +groupName=bar.example.com
25+
package foo
26+
27+
import (
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
)
30+
31+
// FooSpec is the spec of Foo.
32+
type FooSpec struct {
33+
// SomeString is a string.
34+
SomeString string `json:"someString"`
35+
}
36+
37+
// FooStatus is the status of Foo.
38+
type FooStatus struct {
39+
// +Metrics:stateset:name="status_condition",help="The condition of a foo.",labelName="status",JSONPath=".status",list={"True","False","Unknown"},labelsFromPath={"type":".type"}
40+
// +Metrics:gauge:name="status_condition_last_transition_time",help="The condition last transition time of a foo.",valueFrom=.lastTransitionTime,labelsFromPath={"type":".type","status":".status"}
41+
Conditions []Condition `json:"conditions,omitempty"`
42+
}
43+
44+
// Foo is a test object.
45+
// +Metrics:gvk:namePrefix="foo"
46+
// +Metrics:labelFromPath:name="name",JSONPath=".metadata.name"
47+
// +Metrics:gauge:name="created",JSONPath=".metadata.creationTimestamp",help="Unix creation timestamp."
48+
// +Metrics:info:name="owner",JSONPath=".metadata.ownerReferences",help="Owner references.",labelsFromPath={owner_is_controller:".controller",owner_kind:".kind",owner_name:".name",owner_uid:".uid"}
49+
// +Metrics:labelFromPath:name="cluster_name",JSONPath=.metadata.labels.cluster\.x-k8s\.io/cluster-name
50+
type Foo struct {
51+
// TypeMeta comments should NOT appear in the CRD spec
52+
metav1.TypeMeta `json:",inline"`
53+
// ObjectMeta comments should NOT appear in the CRD spec
54+
metav1.ObjectMeta `json:"metadata,omitempty"`
55+
56+
// Spec comments SHOULD appear in the CRD spec
57+
Spec FooSpec `json:"spec,omitempty"`
58+
// Status comments SHOULD appear in the CRD spec
59+
Status FooStatus `json:"status,omitempty"`
60+
}
61+
62+
// Condition is a test condition.
63+
type Condition struct {
64+
// Type of condition.
65+
Type string `json:"type"`
66+
// Status of condition.
67+
Status string `json:"status"`
68+
// LastTransitionTime of condition.
69+
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
70+
}

0 commit comments

Comments
 (0)