From bce28b8663a484e74d8437f3b62d7c30f8560db3 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 20 Aug 2025 10:39:14 +0300 Subject: [PATCH] unstructured spec --- pkg/apis/datasource/v0alpha1/datasource.go | 73 ++++++++ pkg/apis/datasource/v0alpha1/unstructured.go | 173 +++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 pkg/apis/datasource/v0alpha1/datasource.go create mode 100644 pkg/apis/datasource/v0alpha1/unstructured.go diff --git a/pkg/apis/datasource/v0alpha1/datasource.go b/pkg/apis/datasource/v0alpha1/datasource.go new file mode 100644 index 00000000000..54368f4b9ee --- /dev/null +++ b/pkg/apis/datasource/v0alpha1/datasource.go @@ -0,0 +1,73 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// +k8s:deepcopy-gen=true +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DataSource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + // DataSource configuration -- these properties are all visible + // to anyone able to query the data source from their browser + Spec UnstructuredSpec `json:"spec"` + + // Secure values allows setting values that are never shown to users + // The returned properties are only the names of the configured values + Secure common.InlineSecureValues `json:"secure,omitzero,omitempty"` +} + +// DsAccess represents how the datasource connects to the remote service +// +k8s:openapi-gen=true +// +enum +type DsAccess string + +const ( + // The frontend can connect directly to the remote URL + // This method is discouraged + DsAccessDirect DsAccess = "direct" + + // Connect to the remote datasource through the grafana backend + DsAccessProxy DsAccess = "proxy" +) + +func (dsa DsAccess) String() string { + return string(dsa) +} + +// +k8s:openapi-gen=true +type GenericDataSourceSpec struct { + // The display name (previously saved as the "name" property) + Title string `json:"title"` + + Access DsAccess `json:"access,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + IsDefault bool `json:"isDefault,omitempty"` + + // Server URL + URL string `json:"url,omitempty"` + + User string `json:"user,omitempty"` + Database string `json:"database,omitempty"` + BasicAuth bool `json:"basicAuth,omitempty"` + BasicAuthUser string `json:"basicAuthUser,omitempty"` + WithCredentials bool `json:"withCredentials,omitempty"` + + // Generic unstructured configuration settings + JsonData common.Unstructured `json:"jsonData,omitzero"` +} + +// +k8s:deepcopy-gen=true +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DataSourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []DataSource `json:"items"` +} diff --git a/pkg/apis/datasource/v0alpha1/unstructured.go b/pkg/apis/datasource/v0alpha1/unstructured.go new file mode 100644 index 00000000000..8d43a16f9e1 --- /dev/null +++ b/pkg/apis/datasource/v0alpha1/unstructured.go @@ -0,0 +1,173 @@ +package v0alpha1 + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + openapi "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// UnstructuredSpec allows any property to be saved into the spec +// Validation will happen from the dynamically loaded schemas for each datasource +// +k8s:deepcopy-gen=true +// +k8s:openapi-gen=true +type UnstructuredSpec common.Unstructured + +func (u *UnstructuredSpec) GetString(key string) string { + if u.Object == nil { + return "" + } + v := u.Object[key] + str, _ := v.(string) + return str +} + +func (u *UnstructuredSpec) Set(key string, val any) *UnstructuredSpec { + if u.Object == nil { + u.Object = make(map[string]any) + } + if val == nil || val == "" { + delete(u.Object, key) + } else { + u.Object[key] = val + } + return u +} + +func (u *UnstructuredSpec) Title() string { + return u.GetString("title") +} + +func (u *UnstructuredSpec) SetTitle(v string) *UnstructuredSpec { + return u.Set("title", v) +} + +func (u *UnstructuredSpec) URL() string { + return u.GetString("url") +} + +func (u *UnstructuredSpec) SetURL(v string) *UnstructuredSpec { + return u.Set("url", v) +} + +func (u *UnstructuredSpec) Database() string { + return u.GetString("database") +} + +func (u *UnstructuredSpec) SetDatabase(v string) *UnstructuredSpec { + return u.Set("database", v) +} + +func (u *UnstructuredSpec) Access() DsAccess { + return DsAccess(u.GetString("access")) +} + +func (u *UnstructuredSpec) SetAccess(v string) *UnstructuredSpec { + return u.Set("access", v) +} + +func (u *UnstructuredSpec) User() string { + return u.GetString("user") +} + +func (u *UnstructuredSpec) SetUser(v string) *UnstructuredSpec { + return u.Set("user", v) +} + +func (u *UnstructuredSpec) BasicAuth() bool { + v, _, _ := unstructured.NestedBool(u.Object, "basicAuth") + return v +} + +func (u *UnstructuredSpec) SetBasicAuth(v bool) *UnstructuredSpec { + return u.Set("basicAuth", v) +} + +func (u *UnstructuredSpec) BasicAuthUser() string { + return u.GetString("basicAuthUser") +} + +func (u *UnstructuredSpec) SetBasicAuthUser(v string) *UnstructuredSpec { + return u.Set("basicAuthUser", v) +} + +func (u *UnstructuredSpec) WithCredentials() bool { + v, _, _ := unstructured.NestedBool(u.Object, "withCredentials") + return v +} + +func (u *UnstructuredSpec) SetWithCredentials(v bool) *UnstructuredSpec { + return u.Set("withCredentials", v) +} + +func (u *UnstructuredSpec) IsDefault() bool { + v, _, _ := unstructured.NestedBool(u.Object, "isDefault") + return v +} + +func (u *UnstructuredSpec) SetIsDefault(v bool) *UnstructuredSpec { + return u.Set("isDefault", v) +} + +func (u *UnstructuredSpec) ReadOnly() bool { + v, _, _ := unstructured.NestedBool(u.Object, "readOnly") + return v +} + +func (u *UnstructuredSpec) SetReadOnly(v bool) *UnstructuredSpec { + return u.Set("readOnly", v) +} + +func (u *UnstructuredSpec) JSONData() any { + return u.Object["jsonData"] +} + +func (u *UnstructuredSpec) SetJSONData(v any) *UnstructuredSpec { + return u.Set("jsonData", v) +} + +// The OpenAPI spec uses the generated values from GenericDataSourceSpec, except that it: +// 1. Allows additional properties at the root +// 2. The jsonData field *may* be an raw value OR a map +func (UnstructuredSpec) OpenAPIDefinition() openapi.OpenAPIDefinition { + s := schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(func(path string) spec.Ref { + return spec.MustCreateRef(path) + }) + s.Schema.AdditionalProperties = &spec.SchemaOrBool{ + Allows: true, + } + return s +} + +// MarshalJSON ensures that the unstructured object produces proper +// JSON when passed to Go's standard JSON library. +func (u *UnstructuredSpec) MarshalJSON() ([]byte, error) { + return json.Marshal(u.Object) +} + +// UnmarshalJSON ensures that the unstructured object properly decodes +// JSON when passed to Go's standard JSON library. +func (u *UnstructuredSpec) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &u.Object) +} + +func (u *UnstructuredSpec) DeepCopy() *UnstructuredSpec { + if u == nil { + return nil + } + out := new(UnstructuredSpec) + *out = *u + + tmp := common.Unstructured{Object: u.Object} + copy := tmp.DeepCopy() + out.Object = copy.Object + return out +} + +func (u *UnstructuredSpec) DeepCopyInto(out *UnstructuredSpec) { + clone := u.DeepCopy() + *out = *clone +}