Skip to content

Commit 9bd4baa

Browse files
diurnalistmatt-delaney-cruisetheSuessBaarsgaard
authored
feat: add GrafanaLibraryPanel CRD and reconciler (#1858)
* add grafanalibrarypanel CRD types this adds a new GrafanaLibraryPanel CRD, updates all the auto-generated assets derived from the CRD type, and updates existing RBAC and Helm definitions to reference the new type. Co-authored-by: Matt Delaney <matt.delaney@getcruise.com> * track library panels in grafana CR this registers the new CRD with the Grafana CRD so that we can track which library panels have been provisioned with the operator, similar to how other resources are tracked. Co-authored-by: Matt Delaney <matt.delaney@getcruise.com> * add reconciler implementation Co-authored-by: Matt Delaney <matt.delaney@getcruise.com> * disallow fetching library panels from grafana.com much of the content resolving/fetching logic is shared b/w dashboards and library panels, but there is one awkward thing where library panels are not currently (to my knowledge) available via grafana.com. rather than extract just that fetch logic out and only apply it to dashboards, i've opted to simply disable them as a source. an error will be issued if a user tries to configure this field. it is not the most graceful way to handle this, as it's not communicated via the CR validation, but given the effort required to bend the code to do this "right", and given the likelihood of somebody trying to configure a library panel via a grafana.com url (which does not exist right now), we are choosing the easy button. * remove dashboard name from generic functions * add chainsaw tests * mark proposal as decided * enable separate metrics for content resources to enable dashboards and library panels to track their fetch operations separately, split up the metrics by resource. this is accomplished by adding a new interface method to the GrafanaContentResource, and the returned struct holds references to the relevant metrics, if any. this also adds an additional guard to the http round tripper so that if no metric is defined, we don't panic. * remove lazy folder creation and simplify logic * move metrics out of api module since #1856, the api module is separated. this poses problems with the approach to have the CRs expose which metrics correspond to their types via the GrafanaContentMetrics() function, because we create an import loop. recognizing that we're measuring essentially the same thing b/w library panels and dashboards, this updates the metrics to indicate this more explicitly. the following changes are made to metrics: `grafana_operator_dashboard_requests`: * renamed to `grafana_operator_content_requests`, but will always have a label `kind="GrafanaDashboard"`. * renamed `dashboard` label to `resource` `grafana_operator_dashboard_revision_requests`: * renamed to `grafana_operator_revision_requests`, but will always have a label `kind="GrafanaDashboard"`. * renamed `dashboard` label to `resource` * add backwards-compatibility for metrics and simplify reconcile to make the dashboard url request metric backwards compatible, this updates the signature of the roundtripper to be a bit more general, so we can pass a function that is called at the end of the response rather than explicitly increment a metric. this allows us to increment multiple metrics in the case of the dashboard url request, one for the old and one for the new. * refactor: pass metrics to round_tripper directly * refactor: remove intermediate structs to further simplify logic * register invalidspec condition on validation errs * Update api/v1beta1/grafanalibrarypanel_types.go Co-authored-by: Steffen Baarsgaard <steff.bpoulsen@gmail.com> * regen --------- Co-authored-by: Matt Delaney <matt.delaney@getcruise.com> Co-authored-by: Dominik Süß <dominik@suess.wtf> Co-authored-by: Steffen Baarsgaard <steff.bpoulsen@gmail.com>
1 parent 90841cd commit 9bd4baa

33 files changed

+3147
-224
lines changed

api/v1beta1/grafana_types.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,15 @@ type GrafanaPreferences struct {
131131

132132
// GrafanaStatus defines the observed state of Grafana
133133
type GrafanaStatus struct {
134-
Stage OperatorStageName `json:"stage,omitempty"`
135-
StageStatus OperatorStageStatus `json:"stageStatus,omitempty"`
136-
LastMessage string `json:"lastMessage,omitempty"`
137-
AdminUrl string `json:"adminUrl,omitempty"`
138-
Dashboards NamespacedResourceList `json:"dashboards,omitempty"`
139-
Datasources NamespacedResourceList `json:"datasources,omitempty"`
140-
Folders NamespacedResourceList `json:"folders,omitempty"`
141-
Version string `json:"version,omitempty"`
134+
Stage OperatorStageName `json:"stage,omitempty"`
135+
StageStatus OperatorStageStatus `json:"stageStatus,omitempty"`
136+
LastMessage string `json:"lastMessage,omitempty"`
137+
AdminUrl string `json:"adminUrl,omitempty"`
138+
Dashboards NamespacedResourceList `json:"dashboards,omitempty"`
139+
Datasources NamespacedResourceList `json:"datasources,omitempty"`
140+
Folders NamespacedResourceList `json:"folders,omitempty"`
141+
LibraryPanels NamespacedResourceList `json:"libraryPanels,omitempty"`
142+
Version string `json:"version,omitempty"`
142143
}
143144

144145
// +kubebuilder:object:root=true

api/v1beta1/grafanadashboard_types.go

+2
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ func (in *GrafanaDashboard) GrafanaContentStatus() *GrafanaContentStatus {
124124
return &in.Status.GrafanaContentStatus
125125
}
126126

127+
var _ GrafanaContentResource = &GrafanaDashboard{}
128+
127129
func (in *GrafanaDashboard) IsAllowCrossNamespaceImport() bool {
128130
return in.Spec.AllowCrossNamespaceImport
129131
}
+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package v1beta1
2+
3+
import (
4+
"time"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
)
8+
9+
// GrafanaLibraryPanelSpec defines the desired state of GrafanaLibraryPanel
10+
// +kubebuilder:validation:XValidation:rule="(has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) || !(has(self.folderRef) && (has(self.folderUID)))", message="Only one of folderUID or folderRef can be declared at the same time"
11+
// +kubebuilder:validation:XValidation:rule="((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) && has(self.uid)))", message="spec.uid is immutable"
12+
type GrafanaLibraryPanelSpec struct {
13+
GrafanaCommonSpec `json:",inline"`
14+
GrafanaContentSpec `json:",inline"`
15+
16+
// UID of the target folder for this dashboard
17+
// +optional
18+
FolderUID string `json:"folderUID,omitempty"`
19+
20+
// Name of a `GrafanaFolder` resource in the same namespace
21+
// +optional
22+
FolderRef string `json:"folderRef,omitempty"`
23+
24+
// plugins
25+
// +optional
26+
Plugins PluginList `json:"plugins,omitempty"`
27+
}
28+
29+
// GrafanaLibraryPanelStatus defines the observed state of GrafanaLibraryPanel
30+
type GrafanaLibraryPanelStatus struct {
31+
GrafanaCommonStatus `json:",inline"`
32+
GrafanaContentStatus `json:",inline"`
33+
}
34+
35+
//+kubebuilder:object:root=true
36+
//+kubebuilder:subresource:status
37+
38+
// GrafanaLibraryPanel is the Schema for the grafanalibrarypanels API
39+
// +kubebuilder:printcolumn:name="Last resync",type="date",format="date-time",JSONPath=".status.lastResync",description=""
40+
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
41+
// +kubebuilder:resource:categories={grafana-operator}
42+
type GrafanaLibraryPanel struct {
43+
metav1.TypeMeta `json:",inline"`
44+
metav1.ObjectMeta `json:"metadata,omitempty"`
45+
46+
Spec GrafanaLibraryPanelSpec `json:"spec,omitempty"`
47+
Status GrafanaLibraryPanelStatus `json:"status,omitempty"`
48+
}
49+
50+
//+kubebuilder:object:root=true
51+
52+
// GrafanaLibraryPanelList contains a list of GrafanaLibraryPanel
53+
type GrafanaLibraryPanelList struct {
54+
metav1.TypeMeta `json:",inline"`
55+
metav1.ListMeta `json:"metadata,omitempty"`
56+
Items []GrafanaLibraryPanel `json:"items"`
57+
}
58+
59+
// FolderRef implements FolderReferencer.
60+
func (in *GrafanaLibraryPanel) FolderRef() string {
61+
return in.Spec.FolderRef
62+
}
63+
64+
// FolderUID implements FolderReferencer.
65+
func (in *GrafanaLibraryPanel) FolderUID() string {
66+
return in.Spec.FolderUID
67+
}
68+
69+
// FolderNamespace implements FolderReferencer.
70+
func (in *GrafanaLibraryPanel) FolderNamespace() string {
71+
return in.Namespace
72+
}
73+
74+
// Conditions implements FolderReferencer.
75+
func (in *GrafanaLibraryPanel) Conditions() *[]metav1.Condition {
76+
return &in.Status.Conditions
77+
}
78+
79+
// CurrentGeneration implements FolderReferencer.
80+
func (in *GrafanaLibraryPanel) CurrentGeneration() int64 {
81+
return in.Generation
82+
}
83+
84+
func (in *GrafanaLibraryPanel) ResyncPeriodHasElapsed() bool {
85+
deadline := in.Status.LastResync.Add(in.Spec.ResyncPeriod.Duration)
86+
return time.Now().After(deadline)
87+
}
88+
89+
func (in *GrafanaLibraryPanel) MatchLabels() *metav1.LabelSelector {
90+
return in.Spec.InstanceSelector
91+
}
92+
93+
func (in *GrafanaLibraryPanel) MatchNamespace() string {
94+
return in.ObjectMeta.Namespace
95+
}
96+
97+
func (in *GrafanaLibraryPanel) AllowCrossNamespace() bool {
98+
return in.Spec.AllowCrossNamespaceImport
99+
}
100+
101+
// GrafanaContentSpec implements GrafanaContentResource
102+
func (in *GrafanaLibraryPanel) GrafanaContentSpec() *GrafanaContentSpec {
103+
return &in.Spec.GrafanaContentSpec
104+
}
105+
106+
// GrafanaContentSpec implements GrafanaContentResource
107+
func (in *GrafanaLibraryPanel) GrafanaContentStatus() *GrafanaContentStatus {
108+
return &in.Status.GrafanaContentStatus
109+
}
110+
111+
var _ GrafanaContentResource = &GrafanaLibraryPanel{}
112+
113+
func (in *GrafanaLibraryPanelList) Find(namespace string, name string) *GrafanaLibraryPanel {
114+
for _, e := range in.Items {
115+
if e.Namespace == namespace && e.Name == name {
116+
return &e
117+
}
118+
}
119+
return nil
120+
}
121+
122+
func init() {
123+
SchemeBuilder.Register(&GrafanaLibraryPanel{}, &GrafanaLibraryPanelList{})
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package v1beta1
2+
3+
import (
4+
"context"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
)
10+
11+
func newLibraryPanel(name string, uid string) *GrafanaLibraryPanel {
12+
return &GrafanaLibraryPanel{
13+
TypeMeta: v1.TypeMeta{
14+
APIVersion: APIVersion,
15+
Kind: "GrafanaLibraryPanel",
16+
},
17+
ObjectMeta: v1.ObjectMeta{
18+
Name: name,
19+
Namespace: "default",
20+
},
21+
Spec: GrafanaLibraryPanelSpec{
22+
GrafanaCommonSpec: GrafanaCommonSpec{
23+
InstanceSelector: &v1.LabelSelector{
24+
MatchLabels: map[string]string{
25+
"test": "datasource",
26+
},
27+
},
28+
},
29+
GrafanaContentSpec: GrafanaContentSpec{
30+
CustomUID: uid,
31+
Json: "",
32+
},
33+
},
34+
}
35+
}
36+
37+
var _ = Describe("LibraryPanel type", func() {
38+
Context("Ensure LibraryPanel spec.uid is immutable", func() {
39+
ctx := context.Background()
40+
41+
It("Should block adding uid field when missing", func() {
42+
dash := newLibraryPanel("missing-uid", "")
43+
By("Create new LibraryPanel without uid")
44+
Expect(k8sClient.Create(ctx, dash)).To(Succeed())
45+
46+
By("Adding a uid")
47+
dash.Spec.CustomUID = "new-library-panel-uid"
48+
Expect(k8sClient.Update(ctx, dash)).To(HaveOccurred())
49+
})
50+
51+
It("Should block removing uid field when set", func() {
52+
dash := newLibraryPanel("existing-uid", "existing-uid")
53+
By("Creating LibraryPanel with existing UID")
54+
Expect(k8sClient.Create(ctx, dash)).To(Succeed())
55+
56+
By("And setting UID to ''")
57+
dash.Spec.CustomUID = ""
58+
Expect(k8sClient.Update(ctx, dash)).To(HaveOccurred())
59+
})
60+
61+
It("Should block changing value of uid", func() {
62+
dash := newLibraryPanel("removing-uid", "existing-uid")
63+
By("Create new LibraryPanel with existing UID")
64+
Expect(k8sClient.Create(ctx, dash)).To(Succeed())
65+
66+
By("Changing the existing UID")
67+
dash.Spec.CustomUID = "new-library-panel-uid"
68+
Expect(k8sClient.Update(ctx, dash)).To(HaveOccurred())
69+
})
70+
})
71+
})

api/v1beta1/zz_generated.deepcopy.go

+103
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)