Merge remote-tracking branch 'origin/main' into resource-store-bridge

This commit is contained in:
Ryan McKinley 2024-07-03 15:44:27 -07:00
commit 8dd8bc85a6
12 changed files with 261 additions and 51 deletions

View File

@ -4,6 +4,7 @@ go 1.21.10
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/google/go-cmp v0.6.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1
github.com/prometheus/client_golang v1.19.0
github.com/stretchr/testify v1.9.0
@ -11,7 +12,9 @@ require (
k8s.io/apimachinery v0.29.3
k8s.io/apiserver v0.29.2
k8s.io/client-go v0.29.3
k8s.io/component-base v0.29.2
k8s.io/klog/v2 v2.120.1
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
)
require (
@ -34,7 +37,6 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240416155748-26353dc0451f // indirect
github.com/google/uuid v1.6.0 // indirect
@ -84,9 +86,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.29.3 // indirect
k8s.io/component-base v0.29.2 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect

View File

@ -12,63 +12,75 @@ import (
)
type Key struct {
Group string
Resource string
Namespace string
Name string
Group string `json:"group,omitempty"`
Resource string `json:"resource"`
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
}
func ParseKey(key string) (*Key, error) {
// /<group>/<resource>[/namespaces/<namespace>][/<name>]
parts := strings.Split(key, "/")
if len(parts) < 3 {
return nil, fmt.Errorf("invalid key (expecting at least 2 parts): %s", key)
// ParseKey parses a key string into a Key.
// Format: [/group/<group>]/resource/<resource>[/namespace/<namespace>][/name/<name>]
func ParseKey(raw string) (*Key, error) {
parts := strings.Split(raw, "/")
key := &Key{}
// Skip the first empty string
if parts[0] == "" {
parts = parts[1:]
}
if parts[0] != "" {
return nil, fmt.Errorf("invalid key (expecting leading slash): %s", key)
for i := 0; i < len(parts); i += 2 {
k := parts[i]
if i+1 >= len(parts) {
return nil, fmt.Errorf("invalid key: %s", raw)
}
v := parts[i+1]
switch k {
case "group":
key.Group = v
case "resource":
key.Resource = v
case "namespace":
key.Namespace = v
case "name":
key.Name = v
default:
return nil, fmt.Errorf("invalid key name: %s", key)
}
}
k := &Key{
Group: parts[1],
Resource: parts[2],
if len(key.Resource) == 0 {
return nil, fmt.Errorf("missing resource: %s", raw)
}
if len(parts) == 3 {
return k, nil
}
if parts[3] != "namespaces" {
k.Name = parts[3]
return k, nil
}
if len(parts) < 5 {
return nil, fmt.Errorf("invalid key (expecting namespace after 'namespaces'): %s", key)
}
k.Namespace = parts[4]
if len(parts) == 5 {
return k, nil
}
k.Name = parts[5]
return k, nil
return key, nil
}
// String returns the string representation of the Key.
func (k *Key) String() string {
s := "/" + k.Group + "/" + k.Resource
var builder strings.Builder
if len(k.Group) > 0 {
builder.WriteString("/group/")
builder.WriteString(k.Group)
}
if len(k.Resource) > 0 {
builder.WriteString("/resource/")
builder.WriteString(k.Resource)
}
if len(k.Namespace) > 0 {
s += "/namespaces/" + k.Namespace
builder.WriteString("/namespace/")
builder.WriteString(k.Namespace)
}
if len(k.Name) > 0 {
s += "/" + k.Name
builder.WriteString("/name/")
builder.WriteString(k.Name)
}
return s
return builder.String()
}
// IsEqual returns true if the keys are equal.
func (k *Key) IsEqual(other *Key) bool {
return k.Group == other.Group &&
k.Resource == other.Resource &&

View File

@ -0,0 +1,120 @@
package generic
import (
"reflect"
"testing"
)
func TestParseKey(t *testing.T) {
tests := []struct {
name string
raw string
expected *Key
wantErr bool
}{
{
name: "All keys",
raw: "/group/test-group/resource/test-resource/namespace/test-namespace/name/test-name",
expected: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
wantErr: false,
},
{
name: "Missing group",
raw: "/resource/test-resource/namespace/test-namespace/name/test-name",
expected: &Key{Group: "", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
wantErr: false,
},
{
name: "Missing namespace",
raw: "/group/test-group/resource/test-resource/name/test-name",
expected: &Key{Group: "test-group", Resource: "test-resource", Namespace: "", Name: "test-name"},
wantErr: false,
},
{
name: "Missing name",
raw: "/group/test-group/resource/test-resource/namespace/test-namespace",
expected: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: ""},
wantErr: false,
},
{
name: "Missing resource",
raw: "/group/test-group/namespace/test-namespace/name/test-name",
expected: nil,
wantErr: true,
},
{
name: "Empty string",
raw: "",
expected: nil,
wantErr: true,
},
{
name: "Invalid key",
raw: "/",
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseKey(tt.raw)
if (err != nil) != tt.wantErr {
t.Errorf("ParseKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("ParseKey() = %v, expected %v", got, tt.expected)
}
})
}
}
func BenchmarkKey_String(b *testing.B) {
key := &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"}
for i := 0; i < b.N; i++ {
_ = key.String()
}
}
func TestKey_String(t *testing.T) {
tests := []struct {
name string
key *Key
expected string
}{
{
name: "All fields",
key: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
expected: "/group/test-group/resource/test-resource/namespace/test-namespace/name/test-name",
},
{
name: "Missing group",
key: &Key{Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
expected: "/resource/test-resource/namespace/test-namespace/name/test-name",
},
{
name: "Missing namespace",
key: &Key{Group: "test-group", Resource: "test-resource", Name: "test-name"},
expected: "/group/test-group/resource/test-resource/name/test-name",
},
{
name: "Missing name",
key: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace"},
expected: "/group/test-group/resource/test-resource/namespace/test-namespace",
},
{
name: "Missing resource",
key: &Key{Group: "test-group", Namespace: "test-namespace", Name: "test-name"},
expected: "/group/test-group/namespace/test-namespace/name/test-name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.key.String()
if got != tt.expected {
t.Errorf("Key.String() = %s, expected %s", got, tt.expected)
}
})
}
}

View File

@ -86,7 +86,7 @@ func TestResourceToEntity(t *testing.T) {
},
},
},
expectedKey: "/playlist.grafana.app/playlists/namespaces/default/test-name",
expectedKey: "/group/playlist.grafana.app/resource/playlists/namespace/default/name/test-name",
expectedGroupVersion: "v0alpha1",
expectedName: "test-name",
expectedNamespace: "default",
@ -157,7 +157,7 @@ func TestEntityToResource(t *testing.T) {
}{
{
entity: &entityStore.Entity{
Key: "/playlist.grafana.app/playlists/namespaces/default/test-uid",
Key: "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/test-uid",
GroupVersion: "v0alpha1",
Name: "test-uid",
Title: "A playlist",

View File

@ -6,7 +6,7 @@
"namespace": "default",
"name": "adnj1llchbbi8a",
"group_version": "v0alpha1",
"key": "/playlist.grafana.app/playlists/namespaces/default/adnj1llchbbi8a",
"key": "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/adnj1llchbbi8a",
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoiYWRuajFsbGNoYmJpOGEiLCJuYW1lc3BhY2UiOiJkZWZhdWx0IiwidWlkIjoiYjAxOTljNjAtNWYzYS00MWJlLTliYTYtN2E1MmYxZGU4M2ZmIiwiY3JlYXRpb25UaW1lc3RhbXAiOiIyMDI0LTA2LTAyVDAzOjI4OjE3WiIsImFubm90YXRpb25zIjp7ImdyYWZhbmEuYXBwL29yaWdpbktleSI6IjIiLCJncmFmYW5hLmFwcC9vcmlnaW5OYW1lIjoiU1FMIiwiZ3JhZmFuYS5hcHAvb3JpZ2luVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oiLCJncmFmYW5hLmFwcC91cGRhdGVkVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oifX19",
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoiYWRuajFsbGNoYmJpOGEiLCJuYW1lc3BhY2UiOiJkZWZhdWx0IiwidWlkIjoiYjAxOTljNjAtNWYzYS00MWJlLTliYTYtN2E1MmYxZGU4M2ZmIiwiY3JlYXRpb25UaW1lc3RhbXAiOiIyMDI0LTA2LTAyVDAzOjI4OjE3WiIsImFubm90YXRpb25zIjp7ImdyYWZhbmEuYXBwL29yaWdpbktleSI6IjIiLCJncmFmYW5hLmFwcC9vcmlnaW5OYW1lIjoiU1FMIiwiZ3JhZmFuYS5hcHAvb3JpZ2luVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oiLCJncmFmYW5hLmFwcC91cGRhdGVkVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oifX0sInNwZWMiOnsidGl0bGUiOiJ0ZXN0IHBsYXlsaXN0IiwiaW50ZXJ2YWwiOiI1bSIsIml0ZW1zIjpbeyJ0eXBlIjoiZGFzaGJvYXJkX2J5X3VpZCIsInZhbHVlIjoiY2RuaXY1M2dtZDR3MGUifV19fQo=",
"title": "test playlist",

View File

@ -1,3 +1,3 @@
{
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf"
"key": "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/sdfsdfsdf"
}

View File

@ -7,7 +7,7 @@
"namespace": "default",
"name": "sdfsdfsdf",
"group_version": "v0alpha1",
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf",
"key": "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/sdfsdfsdf",
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjNjNzY5YjJlLWFhYTctNDZmNi1hYjgzLWUwMzgwNTBhNmE3NSIsInJlc291cmNlVmVyc2lvbiI6IjEiLCJjcmVhdGlvblRpbWVzdGFtcCI6IjIwMjQtMDYtMDJUMDM6NDk6MjlaIiwibWFuYWdlZEZpZWxkcyI6W3sibWFuYWdlciI6Ik1vemlsbGEiLCJvcGVyYXRpb24iOiJVcGRhdGUiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJ0aW1lIjoiMjAyNC0wNi0wMlQwMzo1Mzo1NVoiLCJmaWVsZHNUeXBlIjoiRmllbGRzVjEiLCJmaWVsZHNWMSI6eyJmOnNwZWMiOnsiZjppbnRlcnZhbCI6e30sImY6aXRlbXMiOnt9LCJmOnRpdGxlIjp7fX19fV19fQ==",
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjNjNzY5YjJlLWFhYTctNDZmNi1hYjgzLWUwMzgwNTBhNmE3NSIsInJlc291cmNlVmVyc2lvbiI6IjEiLCJjcmVhdGlvblRpbWVzdGFtcCI6IjIwMjQtMDYtMDJUMDM6NDk6MjlaIiwibWFuYWdlZEZpZWxkcyI6W3sibWFuYWdlciI6Ik1vemlsbGEiLCJvcGVyYXRpb24iOiJVcGRhdGUiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJ0aW1lIjoiMjAyNC0wNi0wMlQwMzo1Mzo1NVoiLCJmaWVsZHNUeXBlIjoiRmllbGRzVjEiLCJmaWVsZHNWMSI6eyJmOnNwZWMiOnsiZjppbnRlcnZhbCI6e30sImY6aXRlbXMiOnt9LCJmOnRpdGxlIjp7fX19fV19LCJzcGVjIjp7InRpdGxlIjoieHpjdnp4Y3Zxd2Vxd2UiLCJpbnRlcnZhbCI6IjVtIiwiaXRlbXMiOlt7InR5cGUiOiJkYXNoYm9hcmRfYnlfdWlkIiwidmFsdWUiOiJjZG5pdjUzZ21kNHcwZSJ9XX19Cg==",
"title": "xzcvzxcvqweqwe",

View File

@ -6,7 +6,7 @@
"namespace": "default",
"name": "sdfsdfsdf",
"group_version": "v0alpha1",
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf",
"key": "/group/playlist.grafana.app/resource/playlists/namespace/default/name/sdfsdfsdf",
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjAyZmVhOGVlLTk2ZDYtNGIzMy04ZGI5LTU5MmI0NzU4NTM4NSIsImNyZWF0aW9uVGltZXN0YW1wIjoiMjAyNC0wNi0wNFQxNToxODozNFoiLCJtYW5hZ2VkRmllbGRzIjpbeyJtYW5hZ2VyIjoiTW96aWxsYSIsIm9wZXJhdGlvbiI6IlVwZGF0ZSIsImFwaVZlcnNpb24iOiJwbGF5bGlzdC5ncmFmYW5hLmFwcC92MGFscGhhMSIsInRpbWUiOiIyMDI0LTA2LTA0VDE1OjE4OjM0WiIsImZpZWxkc1R5cGUiOiJGaWVsZHNWMSIsImZpZWxkc1YxIjp7ImY6c3BlYyI6eyJmOmludGVydmFsIjp7fSwiZjppdGVtcyI6e30sImY6dGl0bGUiOnt9fX19XX19",
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjAyZmVhOGVlLTk2ZDYtNGIzMy04ZGI5LTU5MmI0NzU4NTM4NSIsImNyZWF0aW9uVGltZXN0YW1wIjoiMjAyNC0wNi0wNFQxNToxODozNFoiLCJtYW5hZ2VkRmllbGRzIjpbeyJtYW5hZ2VyIjoiTW96aWxsYSIsIm9wZXJhdGlvbiI6IlVwZGF0ZSIsImFwaVZlcnNpb24iOiJwbGF5bGlzdC5ncmFmYW5hLmFwcC92MGFscGhhMSIsInRpbWUiOiIyMDI0LTA2LTA0VDE1OjE4OjM0WiIsImZpZWxkc1R5cGUiOiJGaWVsZHNWMSIsImZpZWxkc1YxIjp7ImY6c3BlYyI6eyJmOmludGVydmFsIjp7fSwiZjppdGVtcyI6e30sImY6dGl0bGUiOnt9fX19XX0sInNwZWMiOnsidGl0bGUiOiJ4emN2enhjdiIsImludGVydmFsIjoiNW0iLCJpdGVtcyI6W3sidHlwZSI6ImRhc2hib2FyZF9ieV91aWQiLCJ2YWx1ZSI6ImNkbml2NTNnbWQ0dzBlIn1dfX0K",
"title": "xzcvzxcv",

View File

@ -42,7 +42,7 @@ func TestIntegrationTestDatasource(t *testing.T) {
t.Run("Check discovery client", func(t *testing.T) {
disco := helper.GetGroupVersionInfoJSON("testdata.datasource.grafana.app")
// fmt.Printf("%s", string(disco))
// fmt.Printf("%s", disco)
require.JSONEq(t, `[
{

View File

@ -0,0 +1,71 @@
package playlist
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationFoldersApp(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental APIs
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
},
})
t.Run("Check discovery client", func(t *testing.T) {
disco := helper.NewDiscoveryClient()
resources, err := disco.ServerResourcesForGroupVersion("peakq.grafana.app/v0alpha1")
require.NoError(t, err)
v1Disco, err := json.MarshalIndent(resources, "", " ")
require.NoError(t, err)
//fmt.Printf("%s", string(v1Disco))
require.JSONEq(t, `{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "peakq.grafana.app/v0alpha1",
"resources": [
{
"name": "querytemplates",
"singularName": "querytemplate",
"namespaced": true,
"kind": "QueryTemplate",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"name": "querytemplates/render",
"singularName": "",
"namespaced": true,
"kind": "RenderedQuery",
"verbs": [
"get"
]
}
]
}`, string(v1Disco))
})
}

View File

@ -0,0 +1,7 @@
apiVersion: peakq.grafana.app/v0alpha1
kind: QueryTemplate
metadata:
generateName: x # anything is ok here... except yes or true -- they become boolean!
spec:
title: Generated query template
description: A description from here

View File

@ -47,7 +47,7 @@ func TestIntegrationPlaylist(t *testing.T) {
// The accepted verbs will change when dual write is enabled
disco := h.GetGroupVersionInfoJSON("playlist.grafana.app")
// fmt.Printf("%s", string(disco))
// fmt.Printf("%s", disco)
require.JSONEq(t, `[
{
"version": "v0alpha1",