diff --git a/go.mod b/go.mod index 62ab1e851cc..d65c4932069 100644 --- a/go.mod +++ b/go.mod @@ -482,6 +482,8 @@ require ( require k8s.io/code-generator v0.29.1 // @grafana/grafana-app-platform-squad +require github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad + require ( github.com/grafana/sqlds/v3 v3.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index de5e797a0ae..a8b68deaee2 100644 --- a/go.sum +++ b/go.sum @@ -2913,6 +2913,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= +github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693/go.mod h1:6hSY48PjDm4UObWmGLyJE9DxYVKTgR9kbCspXXJEhcU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= diff --git a/pkg/apis/peakq/v0alpha1/doc.go b/pkg/apis/peakq/v0alpha1/doc.go new file mode 100644 index 00000000000..52a9e2be69c --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/doc.go @@ -0,0 +1,6 @@ +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta +// +groupName=peakq.grafana.app + +package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" diff --git a/pkg/apis/peakq/v0alpha1/register.go b/pkg/apis/peakq/v0alpha1/register.go new file mode 100644 index 00000000000..214840fbad5 --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/register.go @@ -0,0 +1,51 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" +) + +const ( + GROUP = "peakq.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + +var QueryTemplateResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "querytemplates", "querytemplate", "QueryTemplate", + func() runtime.Object { return &QueryTemplate{} }, + func() runtime.Object { return &QueryTemplateList{} }, +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} + + // SchemaBuilder is used by standard codegen + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &QueryTemplate{}, + &QueryTemplateList{}, + &RenderedQuery{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/peakq/v0alpha1/types.go b/pkg/apis/peakq/v0alpha1/types.go new file mode 100644 index 00000000000..1e82dca4908 --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/types.go @@ -0,0 +1,123 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana-plugin-sdk-go/data" + + common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type QueryTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec QueryTemplateSpec `json:"spec,omitempty"` +} + +type QueryTemplateSpec struct { + Title string `json:"title"` + + // The variables that can be used to render + // +listType=map + // +listMapKey=key + Variables []TemplateVariable `json:"vars,omitempty"` + + // Output variables + // +listType=set + Targets []Target `json:"targets"` +} + +type Target struct { + // DataType is the returned Dataplane type from the query. + DataType data.FrameType `json:"dataType,omitempty"` + + // DataTypeVersion is the version for the Dataplane type. + // TODO 2[uint] seems to panic, maybe implement DeepCopy on data.FrameTypeVersion? + // DataTypeVersion data.FrameTypeVersion `json:"dataTypeVersion,omitempty"` + + // Variables that will be replaced in the query + Variables map[string][]VariableReplacement `json:"variables"` + + // The raw query: TODO, should be query.GenericQuery + Properties common.Unstructured `json:"properties"` +} + +// TemplateVariable is the definition of a variable that will be interpolated +// in targets. +type TemplateVariable struct { + // Key is the name of the variable. + Key string `json:"key"` + + // DefaultValue is the value to be used when there is no selected value + // during render. + // +listType=atomic + DefaultValues []string `json:"defaultValue"` + + // ValueListDefinition is the object definition used by the FE + // to get a list of possible values to select for render. + ValueListDefinition common.Unstructured `json:"valueListDefinition"` +} + +// QueryVariable is the definition of a variable that will be interpolated +// in targets. +type VariableReplacement struct { + // Path is the location of the property within a target. + // The format for this is not figured out yet (Maybe JSONPath?). + // Idea: ["string", int, "string"] where int indicates array offset + Path string `json:"path"` + + // Positions is a list of where to perform the interpolation + // within targets during render. + // The first string is the Idx of the target as a string, since openAPI + // does not support ints as map keys + Position *Position `json:"position,omitempty"` + + // How values should be interpolated + // See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options + // NOTE: the format parameter is not yet supported! + Format string `json:"format,omitempty"` + + // Keep track of the values from previous iterations + History []ReplacementHistory `json:"history,omitempty"` +} + +type ReplacementHistory struct { + // Who/what made the change + Source string `json:"source,omitempty"` + + // Value before replacement + Previous string `json:"previous"` + + // The value(s) that replaced the section + Replacement []string `json:"replacement"` +} + +// Position is where to do replacement in the targets +// during render. +type Position struct { + // Start is the byte offset within TargetKey's property of the variable. + // It is the start location for replacements). + Start int64 `json:"start"` // TODO: byte, rune? + + // End is the byte offset of the end of the variable. + End int64 `json:"end"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type QueryTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []QueryTemplate `json:"items,omitempty"` +} + +// Dummy object that represents a real query object +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type RenderedQuery struct { + metav1.TypeMeta `json:",inline"` + + // +listType=atomic + Targets []Target `json:"targets,omitempty"` +} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..6079da06587 --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go @@ -0,0 +1,255 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Position) DeepCopyInto(out *Position) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Position. +func (in *Position) DeepCopy() *Position { + if in == nil { + return nil + } + out := new(Position) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QueryTemplate) DeepCopyInto(out *QueryTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplate. +func (in *QueryTemplate) DeepCopy() *QueryTemplate { + if in == nil { + return nil + } + out := new(QueryTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *QueryTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QueryTemplateList) DeepCopyInto(out *QueryTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]QueryTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplateList. +func (in *QueryTemplateList) DeepCopy() *QueryTemplateList { + if in == nil { + return nil + } + out := new(QueryTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *QueryTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QueryTemplateSpec) DeepCopyInto(out *QueryTemplateSpec) { + *out = *in + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make([]TemplateVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]Target, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplateSpec. +func (in *QueryTemplateSpec) DeepCopy() *QueryTemplateSpec { + if in == nil { + return nil + } + out := new(QueryTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RenderedQuery) DeepCopyInto(out *RenderedQuery) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]Target, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RenderedQuery. +func (in *RenderedQuery) DeepCopy() *RenderedQuery { + if in == nil { + return nil + } + out := new(RenderedQuery) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RenderedQuery) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReplacementHistory) DeepCopyInto(out *ReplacementHistory) { + *out = *in + if in.Replacement != nil { + in, out := &in.Replacement, &out.Replacement + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplacementHistory. +func (in *ReplacementHistory) DeepCopy() *ReplacementHistory { + if in == nil { + return nil + } + out := new(ReplacementHistory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Target) DeepCopyInto(out *Target) { + *out = *in + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make(map[string][]VariableReplacement, len(*in)) + for key, val := range *in { + var outVal []VariableReplacement + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]VariableReplacement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + (*out)[key] = outVal + } + } + in.Properties.DeepCopyInto(&out.Properties) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. +func (in *Target) DeepCopy() *Target { + if in == nil { + return nil + } + out := new(Target) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateVariable) DeepCopyInto(out *TemplateVariable) { + *out = *in + if in.DefaultValues != nil { + in, out := &in.DefaultValues, &out.DefaultValues + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.ValueListDefinition.DeepCopyInto(&out.ValueListDefinition) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateVariable. +func (in *TemplateVariable) DeepCopy() *TemplateVariable { + if in == nil { + return nil + } + out := new(TemplateVariable) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VariableReplacement) DeepCopyInto(out *VariableReplacement) { + *out = *in + if in.Position != nil { + in, out := &in.Position, &out.Position + *out = new(Position) + **out = **in + } + if in.History != nil { + in, out := &in.History, &out.History + *out = make([]ReplacementHistory, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VariableReplacement. +func (in *VariableReplacement) DeepCopy() *VariableReplacement { + if in == nil { + return nil + } + out := new(VariableReplacement) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.defaults.go b/pkg/apis/peakq/v0alpha1/zz_generated.defaults.go new file mode 100644 index 00000000000..238fc2f4edc --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/zz_generated.defaults.go @@ -0,0 +1,19 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go b/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go new file mode 100644 index 00000000000..e1a6a829920 --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go @@ -0,0 +1,450 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +// This file was autogenerated by openapi-gen. Do not edit it manually! + +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position": schema_pkg_apis_peakq_v0alpha1_Position(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplate": schema_pkg_apis_peakq_v0alpha1_QueryTemplate(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateList": schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec": schema_pkg_apis_peakq_v0alpha1_QueryTemplateSpec(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery": schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory": schema_pkg_apis_peakq_v0alpha1_ReplacementHistory(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target": schema_pkg_apis_peakq_v0alpha1_Target(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.TemplateVariable": schema_pkg_apis_peakq_v0alpha1_TemplateVariable(ref), + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.VariableReplacement": schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref), + } +} + +func schema_pkg_apis_peakq_v0alpha1_Position(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Position is where to do replacement in the targets during render.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "start": { + SchemaProps: spec.SchemaProps{ + Description: "Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements).", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "end": { + SchemaProps: spec.SchemaProps{ + Description: "End is the byte offset of the end of the variable.", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + Required: []string{"start", "end"}, + }, + }, + } +} + +func schema_pkg_apis_peakq_v0alpha1_QueryTemplate(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplate"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplate", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_peakq_v0alpha1_QueryTemplateSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "vars": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The variables that can be used to render", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.TemplateVariable"), + }, + }, + }, + }, + }, + "targets": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Output variables", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target"), + }, + }, + }, + }, + }, + }, + Required: []string{"title", "targets"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target", "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.TemplateVariable"}, + } +} + +func schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Dummy object that represents a real query object", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "targets": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target"}, + } +} + +func schema_pkg_apis_peakq_v0alpha1_ReplacementHistory(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "source": { + SchemaProps: spec.SchemaProps{ + Description: "Who/what made the change", + Type: []string{"string"}, + Format: "", + }, + }, + "previous": { + SchemaProps: spec.SchemaProps{ + Description: "Value before replacement", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "replacement": { + SchemaProps: spec.SchemaProps{ + Description: "The value(s) that replaced the section", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"previous", "replacement"}, + }, + }, + } +} + +func schema_pkg_apis_peakq_v0alpha1_Target(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "dataType": { + SchemaProps: spec.SchemaProps{ + Description: "DataType is the returned Dataplane type from the query.", + Type: []string{"string"}, + Format: "", + }, + }, + "variables": { + SchemaProps: spec.SchemaProps{ + Description: "Variables that will be replaced in the query", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.VariableReplacement"), + }, + }, + }, + }, + }, + }, + }, + }, + "properties": { + SchemaProps: spec.SchemaProps{ + Description: "The raw query: TODO, should be query.GenericQuery", + Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + }, + }, + }, + Required: []string{"variables", "properties"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured", "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.VariableReplacement"}, + } +} + +func schema_pkg_apis_peakq_v0alpha1_TemplateVariable(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TemplateVariable is the definition of a variable that will be interpolated in targets.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "Key is the name of the variable.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "defaultValue": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "DefaultValue is the value to be used when there is no selected value during render.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "valueListDefinition": { + SchemaProps: spec.SchemaProps{ + Description: "ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render.", + Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + }, + }, + }, + Required: []string{"key", "defaultValue", "valueListDefinition"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"}, + } +} + +func schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "QueryVariable is the definition of a variable that will be interpolated in targets.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "path": { + SchemaProps: spec.SchemaProps{ + Description: "Path is the location of the property within the the target properties. The format for this is not figured out yet (Maybe JSONPath?). Idea: [\"string\", int, \"string\"] where int indicates array offset", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "position": { + SchemaProps: spec.SchemaProps{ + Description: "Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys", + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position"), + }, + }, + "format": { + SchemaProps: spec.SchemaProps{ + Description: "How values should be interpolated See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options NOTE: the format parameter is not yet supported!", + Type: []string{"string"}, + Format: "", + }, + }, + "history": { + SchemaProps: spec.SchemaProps{ + Description: "Keep track of the values from previous iterations", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory"), + }, + }, + }, + }, + }, + }, + Required: []string{"path"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position", "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory"}, + } +} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..9d032f4727f --- /dev/null +++ b/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,4 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,ReplacementHistory,Replacement +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,VariableReplacement,History +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,QueryTemplateSpec,Variables +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,TemplateVariable,DefaultValues diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 2a918867edc..69908da110a 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/registry/apis/peakq" "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/service" @@ -31,6 +32,7 @@ func ProvideRegistryServiceSink( _ *featuretoggle.FeatureFlagAPIBuilder, _ *datasource.DataSourceAPIBuilder, _ *folders.FolderAPIBuilder, + _ *peakq.PeakQAPIBuilder, _ *service.ServiceAPIBuilder, _ *query.QueryAPIBuilder, ) *Service { diff --git a/pkg/registry/apis/peakq/register.go b/pkg/registry/apis/peakq/register.go new file mode 100644 index 00000000000..3be6d11d038 --- /dev/null +++ b/pkg/registry/apis/peakq/register.go @@ -0,0 +1,177 @@ +package peakq + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +var _ builder.APIGroupBuilder = (*PeakQAPIBuilder)(nil) + +// This is used just so wire has something unique to return +type PeakQAPIBuilder struct{} + +func NewPeakQAPIBuilder() *PeakQAPIBuilder { + return &PeakQAPIBuilder{} +} + +func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar) *PeakQAPIBuilder { + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + return nil // skip registration unless opting into experimental apis + } + builder := NewPeakQAPIBuilder() + apiregistration.RegisterAPI(NewPeakQAPIBuilder()) + return builder +} + +func (b *PeakQAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return nil // default authorizer is fine +} + +func (b *PeakQAPIBuilder) GetGroupVersion() schema.GroupVersion { + return peakq.SchemeGroupVersion +} + +func (b *PeakQAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + gv := peakq.SchemeGroupVersion + err := peakq.AddToScheme(scheme) + if err != nil { + return err + } + + // Link this version to the internal representation. + // This is used for server-side-apply (PATCH), and avoids the error: + // "no kind is registered for the type" + // addKnownTypes(scheme, schema.GroupVersion{ + // Group: peakq.GROUP, + // Version: runtime.APIVersionInternal, + // }) + metav1.AddToGroupVersion(scheme, gv) + return scheme.SetVersionPriority(gv) +} + +func (b *PeakQAPIBuilder) GetAPIGroupInfo( + scheme *runtime.Scheme, + codecs serializer.CodecFactory, + optsGetter generic.RESTOptionsGetter, + _ bool, // dual write (not relevant) +) (*genericapiserver.APIGroupInfo, error) { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(peakq.GROUP, scheme, metav1.ParameterCodec, codecs) + + resourceInfo := peakq.QueryTemplateResourceInfo + storage := map[string]rest.Storage{} + peakqStorage, err := newStorage(scheme, optsGetter) + if err != nil { + return nil, err + } + storage[resourceInfo.StoragePath()] = peakqStorage + storage[resourceInfo.StoragePath("render")] = &renderREST{ + getter: peakqStorage, + } + + apiGroupInfo.VersionedResourcesStorageMap[peakq.VERSION] = storage + return &apiGroupInfo, nil +} + +func (b *PeakQAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return peakq.GetOpenAPIDefinitions +} + +// NOT A GREAT APPROACH... BUT will make a UI for statically defined +func (b *PeakQAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + defs := peakq.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} }) + renderedQuerySchema := defs["github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery"].Schema + queryTemplateSpecSchema := defs["github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec"].Schema + + params := []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + // Arbitrary name. It won't appear in the request URL, + // but will be used in code generated from this OAS spec + Name: "variables", + In: "query", + Schema: spec.MapProperty(spec.ArrayProperty(spec.StringProperty())), + Style: "form", + Explode: true, + Description: "Each variable is prefixed with var-{variable}={value}", + Example: map[string][]string{ + "var-metricName": {"up"}, + "var-another": {"first", "second"}, + }, + }, + }, + } + return &builder.APIRoutes{ + Root: []builder.APIRouteHandler{ + { + Path: "render", + Spec: &spec3.PathProps{ + Summary: "an example at the root level", + Description: "longer description here?", + Post: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + Parameters: params, + RequestBody: &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &queryTemplateSpecSchema, + // Example: basicTemplateSpec, + Examples: map[string]*spec3.Example{ + "test": { + ExampleProps: spec3.ExampleProps{ + Summary: "hello", + Value: basicTemplateSpec, + }, + }, + "test2": { + ExampleProps: spec3.ExampleProps{ + Summary: "hello2", + Value: basicTemplateSpec, + }, + }, + }, + }, + }, + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Description: "OK", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &renderedQuerySchema, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: renderPOSTHandler, + }, + }, + } +} diff --git a/pkg/registry/apis/peakq/render.go b/pkg/registry/apis/peakq/render.go new file mode 100644 index 00000000000..f6a62a9e75c --- /dev/null +++ b/pkg/registry/apis/peakq/render.go @@ -0,0 +1,216 @@ +package peakq + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" + "github.com/spyzhov/ajson" +) + +type renderREST struct { + getter rest.Getter +} + +var _ = rest.Connecter(&renderREST{}) + +func (r *renderREST) New() runtime.Object { + return &peakq.RenderedQuery{} +} + +func (r *renderREST) Destroy() { +} + +func (r *renderREST) ConnectMethods() []string { + return []string{"GET"} +} + +func (r *renderREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" // true means you can use the trailing path as a variable +} + +func (r *renderREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + obj, err := r.getter.Get(ctx, name, &v1.GetOptions{}) + if err != nil { + return nil, err + } + template, ok := obj.(*peakq.QueryTemplate) + if !ok { + return nil, fmt.Errorf("expected template") + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + input, err := makeVarMapFromParams(req.URL.Query()) + if err != nil { + responder.Error(err) + return + } + rq, err := Render(template.Spec, input) + if err != nil { + responder.Error(fmt.Errorf("failed to render: %w", err)) + return + } + responder.Object(http.StatusOK, rq) + }), nil +} + +func renderPOSTHandler(w http.ResponseWriter, req *http.Request) { + input, err := makeVarMapFromParams(req.URL.Query()) + if err != nil { + _, _ = w.Write([]byte("ERROR: " + err.Error())) + w.WriteHeader(500) + return + } + + var qT peakq.QueryTemplate + err = json.NewDecoder(req.Body).Decode(&qT.Spec) + if err != nil { + _, _ = w.Write([]byte("ERROR: " + err.Error())) + w.WriteHeader(500) + return + } + results, err := Render(qT.Spec, input) + if err != nil { + _, _ = w.Write([]byte("ERROR: " + err.Error())) + w.WriteHeader(500) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(results) +} + +// Replicate the grafana dashboard URL syntax +// &var-abc=1&var=abc=2&var-xyz=3... +func makeVarMapFromParams(v url.Values) (map[string][]string, error) { + input := make(map[string][]string, len(v)) + for key, vals := range v { + if !strings.HasPrefix(key, "var-") { + continue + } + input[key[4:]] = vals + } + return input, nil +} + +type replacement struct { + *peakq.Position + *peakq.TemplateVariable +} + +func getReplacementMap(qt peakq.QueryTemplateSpec) map[int]map[string][]replacement { + byTargetPath := make(map[int]map[string][]replacement) + + varMap := make(map[string]*peakq.TemplateVariable, len(qt.Variables)) + for i, v := range qt.Variables { + varMap[v.Key] = &qt.Variables[i] + } + + for i, target := range qt.Targets { + if byTargetPath[i] == nil { + byTargetPath[i] = make(map[string][]replacement) + } + for k, vReps := range target.Variables { + for rI, rep := range vReps { + byTargetPath[i][rep.Path] = append(byTargetPath[i][rep.Path], + replacement{ + Position: vReps[rI].Position, + TemplateVariable: varMap[k], + }, + ) + } + } + } + + for idx, byTargetIdx := range byTargetPath { + for path := range byTargetIdx { + sort.Slice(byTargetPath[idx][path], func(i, j int) bool { + return byTargetPath[idx][path][i].Start < byTargetPath[idx][path][j].Start + }) + } + } + + return byTargetPath +} + +func Render(qt peakq.QueryTemplateSpec, selectedValues map[string][]string) (*peakq.RenderedQuery, error) { + targets := qt.DeepCopy().Targets + + rawTargetObjects := make([]*ajson.Node, len(qt.Targets)) + for i, t := range qt.Targets { + b, err := t.Properties.MarshalJSON() + if err != nil { + return nil, err + } + rawTargetObjects[i], err = ajson.Unmarshal(b) + if err != nil { + return nil, err + } + } + + rm := getReplacementMap(qt) + for targetIdx, byTargetIdx := range rm { + for path, reps := range byTargetIdx { + o := rawTargetObjects[targetIdx] + nodes, err := o.JSONPath(path) + if err != nil { + return nil, err + } + if len(nodes) != 1 { + return nil, fmt.Errorf("expected one lead node at path %v but got %v", path, len(nodes)) + } + n := nodes[0] + if !n.IsString() { + return nil, fmt.Errorf("only string type leaf notes supported currently, %v is not a string", path) + } + s := n.String() + s = s[1 : len(s)-1] + var offSet int64 + for _, r := range reps { + // I think breaks with utf...something...? + // TODO: Probably simpler to store the non-template parts and insert the values into that, then don't have to track + // offsets + if r.Position == nil { + return nil, fmt.Errorf("nil position not support yet, will be full replacement") + } + if len(selectedValues[r.Key]) != 1 { + return nil, fmt.Errorf("selected value missing, or more then one provided") + } + value := selectedValues[r.Key][0] + s = s[:r.Start+offSet] + value + s[r.End+offSet:] + offSet = int64(len(value)+int(offSet)) - (r.End - r.Start) + } + if err = n.SetString(s); err != nil { + return nil, err + } + } + } + + for i, aT := range rawTargetObjects { + raw, err := ajson.Marshal(aT) + if err != nil { + return nil, err + } + u := v0alpha1.Unstructured{} + err = u.UnmarshalJSON(raw) + if err != nil { + return nil, err + } + targets[i].Properties = u + } + + return &peakq.RenderedQuery{ + Targets: targets, + }, nil +} diff --git a/pkg/registry/apis/peakq/render_examples.go b/pkg/registry/apis/peakq/render_examples.go new file mode 100644 index 00000000000..289bfde7864 --- /dev/null +++ b/pkg/registry/apis/peakq/render_examples.go @@ -0,0 +1,78 @@ +package peakq + +import ( + "github.com/grafana/grafana-plugin-sdk-go/data" + + common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" +) + +var basicTemplateSpec = peakq.QueryTemplateSpec{ + Title: "Test", + Variables: []peakq.TemplateVariable{ + { + Key: "metricName", + DefaultValues: []string{`down`}, + }, + }, + Targets: []peakq.Target{ + { + DataType: data.FrameTypeUnknown, + //DataTypeVersion: data.FrameTypeVersion{0, 0}, + Variables: map[string][]peakq.VariableReplacement{ + "metricName": { + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 0, + End: 10, + }, + }, + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 13, + End: 23, + }, + }, + }, + }, + + Properties: common.Unstructured{ + Object: map[string]any{ + "refId": "A", // TODO: Set when Where? + "datasource": map[string]any{ + "type": "prometheus", + "uid": "foo", // TODO: Probably a default templating thing to set this. + }, + "editorMode": "builder", + "expr": "metricName + metricName + 42", + "instant": true, + "range": false, + "exemplar": false, + }, + }, + }, + }, +} + +var basicTemplateRenderedTargets = []peakq.Target{ + { + DataType: data.FrameTypeUnknown, + //DataTypeVersion: data.FrameTypeVersion{0, 0}, + Properties: common.Unstructured{ + Object: map[string]any{ + "refId": "A", // TODO: Set when Where? + "datasource": map[string]any{ + "type": "prometheus", + "uid": "foo", // TODO: Probably a default templating thing to set this. + }, + "editorMode": "builder", + "expr": "up + up + 42", + "instant": true, + "range": false, + "exemplar": false, + }, + }, + }, +} diff --git a/pkg/registry/apis/peakq/render_examples_test.go b/pkg/registry/apis/peakq/render_examples_test.go new file mode 100644 index 00000000000..bdcbfc43eb0 --- /dev/null +++ b/pkg/registry/apis/peakq/render_examples_test.go @@ -0,0 +1,17 @@ +package peakq + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRender(t *testing.T) { + rT, err := Render(basicTemplateSpec, map[string][]string{"metricName": {"up"}}) + require.NoError(t, err) + require.Equal(t, basicTemplateRenderedTargets[0].Properties.Object["expr"], rT.Targets[0].Properties.Object["expr"]) + b, _ := json.MarshalIndent(basicTemplateSpec, "", " ") + fmt.Println(string(b)) +} diff --git a/pkg/registry/apis/peakq/render_test.go b/pkg/registry/apis/peakq/render_test.go new file mode 100644 index 00000000000..ea8c1a1ce0c --- /dev/null +++ b/pkg/registry/apis/peakq/render_test.go @@ -0,0 +1,182 @@ +package peakq + +import ( + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" + + common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" +) + +var nestedFieldRender = peakq.QueryTemplateSpec{ + Title: "Test", + Variables: []peakq.TemplateVariable{ + { + Key: "metricName", + }, + }, + Targets: []peakq.Target{ + { + DataType: data.FrameTypeUnknown, + //DataTypeVersion: data.FrameTypeVersion{0, 0}, + + Variables: map[string][]peakq.VariableReplacement{ + "metricName": { + { + Path: "$.nestedObject.anArray[0]", + Position: &peakq.Position{ + Start: 0, + End: 3, + }, + }, + }, + }, + Properties: common.Unstructured{ + Object: map[string]any{ + "nestedObject": map[string]any{ + "anArray": []any{"foo", .2}, + }, + }, + }, + }, + }, +} + +var nestedFieldRenderedTargets = []peakq.Target{ + { + DataType: data.FrameTypeUnknown, + Variables: map[string][]peakq.VariableReplacement{ + "metricName": { + { + Path: "$.nestedObject.anArray[0]", + Position: &peakq.Position{ + Start: 0, + End: 3, + }, + }, + }, + }, + //DataTypeVersion: data.FrameTypeVersion{0, 0}, + Properties: common.Unstructured{ + Object: map[string]any{ + "nestedObject": map[string]any{ + "anArray": []any{"up", .2}, + }, + }, + }, + }, +} + +func TestNestedFieldRender(t *testing.T) { + rT, err := Render(nestedFieldRender, map[string][]string{"metricName": {"up"}}) + require.NoError(t, err) + require.Equal(t, + nestedFieldRenderedTargets, + rT.Targets, + ) +} + +var multiVarTemplate = peakq.QueryTemplateSpec{ + Title: "Test", + Variables: []peakq.TemplateVariable{ + { + Key: "metricName", + }, + { + Key: "anotherMetric", + }, + }, + Targets: []peakq.Target{ + { + DataType: data.FrameTypeUnknown, + //DataTypeVersion: data.FrameTypeVersion{0, 0}, + + Variables: map[string][]peakq.VariableReplacement{ + "metricName": { + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 4, + End: 14, + }, + }, + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 37, + End: 47, + }, + }, + }, + "anotherMetric": { + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 21, + End: 34, + }, + }, + }, + }, + + Properties: common.Unstructured{ + Object: map[string]any{ + "expr": "1 + metricName + 1 + anotherMetric + metricName", + }, + }, + }, + }, +} + +var multiVarRenderedTargets = []peakq.Target{ + { + DataType: data.FrameTypeUnknown, + Variables: map[string][]peakq.VariableReplacement{ + "metricName": { + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 4, + End: 14, + }, + }, + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 37, + End: 47, + }, + }, + }, + "anotherMetric": { + { + Path: "$.expr", + Position: &peakq.Position{ + Start: 21, + End: 34, + }, + }, + }, + }, + //DataTypeVersion: data.FrameTypeVersion{0, 0}, + Properties: common.Unstructured{ + Object: map[string]any{ + "expr": "1 + up + 1 + sloths_do_like_a_good_nap + up", + }, + }, + }, +} + +func TestMultiVarTemplate(t *testing.T) { + rT, err := Render(multiVarTemplate, map[string][]string{ + "metricName": {"up"}, + "anotherMetric": {"sloths_do_like_a_good_nap"}, + }) + require.NoError(t, err) + require.Equal(t, + multiVarRenderedTargets, + rT.Targets, + ) +} diff --git a/pkg/registry/apis/peakq/storage.go b/pkg/registry/apis/peakq/storage.go new file mode 100644 index 00000000000..d65bdbc7717 --- /dev/null +++ b/pkg/registry/apis/peakq/storage.go @@ -0,0 +1,62 @@ +package peakq + +import ( + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + + peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/utils" +) + +var _ grafanarest.Storage = (*storage)(nil) + +type storage struct { + *genericregistry.Store +} + +func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { + strategy := grafanaregistry.NewStrategy(scheme) + + resourceInfo := peakq.QueryTemplateResourceInfo + store := &genericregistry.Store{ + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, + PredicateFunc: grafanaregistry.Matcher, + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: utils.NewTableConverter( + resourceInfo.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string"}, + {Name: "Created At", Type: "date"}, + }, + func(obj any) ([]interface{}, error) { + m, ok := obj.(*peakq.QueryTemplate) + if !ok { + return nil, fmt.Errorf("expected query template") + } + return []interface{}{ + m.Name, + m.Spec.Title, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + }, + ), + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + return &storage{Store: store}, nil +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 3c5758da87b..d85d0fc4119 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/registry/apis/peakq" "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/service" @@ -31,6 +32,7 @@ var WireSet = wire.NewSet( featuretoggle.RegisterAPIService, datasource.RegisterAPIService, folders.RegisterAPIService, + peakq.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, )