mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Use client-go to test legacy playlist changes (#77245)
This commit is contained in:
parent
bf554d121c
commit
9b472b3726
@ -7,12 +7,20 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/yaml"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/server"
|
||||
@ -40,9 +48,6 @@ type K8sTestHelper struct {
|
||||
|
||||
// // Registered groups
|
||||
groups []metav1.APIGroup
|
||||
|
||||
// Used to build the URL paths
|
||||
selectedGVR schema.GroupVersionResource
|
||||
}
|
||||
|
||||
func NewK8sTestHelper(t *testing.T) *K8sTestHelper {
|
||||
@ -67,7 +72,7 @@ func NewK8sTestHelper(t *testing.T) *K8sTestHelper {
|
||||
c.Org2 = c.createTestUsers(int64(2))
|
||||
|
||||
// Read the API groups
|
||||
rsp := doRequest(c, RequestParams{
|
||||
rsp := DoRequest(c, RequestParams{
|
||||
User: c.Org1.Viewer,
|
||||
Path: "/apis",
|
||||
// Accept: "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json",
|
||||
@ -76,6 +81,68 @@ func NewK8sTestHelper(t *testing.T) *K8sTestHelper {
|
||||
return c
|
||||
}
|
||||
|
||||
type ResourceClientArgs struct {
|
||||
User User
|
||||
Namespace string
|
||||
GVR schema.GroupVersionResource
|
||||
}
|
||||
|
||||
type K8sResourceClient struct {
|
||||
t *testing.T
|
||||
Args ResourceClientArgs
|
||||
Resource dynamic.ResourceInterface
|
||||
}
|
||||
|
||||
// This will set the expected Group/Version/Resource and return the discovery info if found
|
||||
func (c *K8sTestHelper) GetResourceClient(args ResourceClientArgs) *K8sResourceClient {
|
||||
c.t.Helper()
|
||||
|
||||
if args.Namespace == "" {
|
||||
args.Namespace = c.namespacer(args.User.Identity.GetOrgID())
|
||||
}
|
||||
|
||||
return &K8sResourceClient{
|
||||
t: c.t,
|
||||
Args: args,
|
||||
Resource: args.User.Client.Resource(args.GVR).Namespace(args.Namespace),
|
||||
}
|
||||
}
|
||||
|
||||
// Cast the error to status error
|
||||
func (c *K8sTestHelper) AsStatusError(err error) *errors.StatusError {
|
||||
c.t.Helper()
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:errorlint
|
||||
statusError, ok := err.(*errors.StatusError)
|
||||
require.True(c.t, ok)
|
||||
return statusError
|
||||
}
|
||||
|
||||
// remove the meta keys that are expected to change each time
|
||||
func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string {
|
||||
c.t.Helper()
|
||||
|
||||
copy := v.DeepCopy().Object
|
||||
meta, ok := copy["metadata"].(map[string]any)
|
||||
require.True(c.t, ok)
|
||||
|
||||
replaceMeta := []string{"creationTimestamp", "resourceVersion", "uid"}
|
||||
for _, key := range replaceMeta {
|
||||
old, ok := meta[key]
|
||||
require.True(c.t, ok)
|
||||
require.NotEmpty(c.t, old)
|
||||
meta[key] = fmt.Sprintf("${%s}", key)
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(copy, "", " ")
|
||||
require.NoError(c.t, err)
|
||||
return string(out)
|
||||
}
|
||||
|
||||
type OrgUsers struct {
|
||||
Admin User
|
||||
Editor User
|
||||
@ -84,6 +151,7 @@ type OrgUsers struct {
|
||||
|
||||
type User struct {
|
||||
Identity identity.Requester
|
||||
Client *dynamic.DynamicClient
|
||||
password string
|
||||
}
|
||||
|
||||
@ -106,13 +174,6 @@ type K8sResponse[T any] struct {
|
||||
type AnyResourceResponse = K8sResponse[AnyResource]
|
||||
type AnyResourceListResponse = K8sResponse[AnyResourceList]
|
||||
|
||||
// This will set the expected Group/Version/Resource and return the discovery info if found
|
||||
func (c *K8sTestHelper) SetGroupVersionResource(gvr schema.GroupVersionResource) {
|
||||
c.t.Helper()
|
||||
|
||||
c.selectedGVR = gvr
|
||||
}
|
||||
|
||||
func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyResource) AnyResourceResponse {
|
||||
c.t.Helper()
|
||||
|
||||
@ -130,7 +191,7 @@ func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyReso
|
||||
body, err := json.Marshal(payload)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
return doRequest(c, RequestParams{
|
||||
return DoRequest(c, RequestParams{
|
||||
Method: http.MethodPost,
|
||||
Path: path,
|
||||
User: user,
|
||||
@ -147,7 +208,7 @@ func (c *K8sTestHelper) PutResource(user User, resource string, payload AnyResou
|
||||
body, err := json.Marshal(payload)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
return doRequest(c, RequestParams{
|
||||
return DoRequest(c, RequestParams{
|
||||
Method: http.MethodPut,
|
||||
Path: path,
|
||||
User: user,
|
||||
@ -155,20 +216,20 @@ func (c *K8sTestHelper) PutResource(user User, resource string, payload AnyResou
|
||||
}, &AnyResource{})
|
||||
}
|
||||
|
||||
func (c *K8sTestHelper) List(user User, namespace string) AnyResourceListResponse {
|
||||
func (c *K8sTestHelper) List(user User, namespace string, gvr schema.GroupVersionResource) AnyResourceListResponse {
|
||||
c.t.Helper()
|
||||
|
||||
return doRequest(c, RequestParams{
|
||||
return DoRequest(c, RequestParams{
|
||||
User: user,
|
||||
Path: fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s",
|
||||
c.selectedGVR.Group,
|
||||
c.selectedGVR.Version,
|
||||
gvr.Group,
|
||||
gvr.Version,
|
||||
namespace,
|
||||
c.selectedGVR.Resource),
|
||||
gvr.Resource),
|
||||
}, &AnyResourceList{})
|
||||
}
|
||||
|
||||
func doRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResponse[T] {
|
||||
func DoRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResponse[T] {
|
||||
c.t.Helper()
|
||||
|
||||
if params.Method == "" {
|
||||
@ -224,12 +285,38 @@ func doRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResp
|
||||
r.Status = s
|
||||
r.Result = nil
|
||||
}
|
||||
} else {
|
||||
_ = yaml.Unmarshal(r.Body, r.Result)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Read local JSON or YAML file into a resource
|
||||
func (c *K8sTestHelper) LoadYAMLOrJSONFile(fpath string) *unstructured.Unstructured {
|
||||
c.t.Helper()
|
||||
|
||||
//nolint:gosec
|
||||
raw, err := os.ReadFile(fpath)
|
||||
require.NoError(c.t, err)
|
||||
require.NotEmpty(c.t, raw)
|
||||
return c.LoadYAMLOrJSON(string(raw))
|
||||
}
|
||||
|
||||
// Read local JSON or YAML file into a resource
|
||||
func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured {
|
||||
c.t.Helper()
|
||||
|
||||
decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(body)), 100)
|
||||
var rawObj runtime.RawExtension
|
||||
err := decoder.Decode(&rawObj)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
obj, _, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil)
|
||||
require.NoError(c.t, err)
|
||||
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
return &unstructured.Unstructured{Object: unstructuredMap}
|
||||
}
|
||||
|
||||
func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers {
|
||||
c.t.Helper()
|
||||
|
||||
@ -252,6 +339,7 @@ func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers {
|
||||
supportbundlestest.NewFakeBundleService())
|
||||
require.NoError(c.t, err)
|
||||
|
||||
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
|
||||
createUser := func(key string, role org.RoleType) User {
|
||||
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
|
||||
DefaultOrgRole: string(role),
|
||||
@ -272,8 +360,19 @@ func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers {
|
||||
require.NoError(c.t, err)
|
||||
require.Equal(c.t, orgId, s.OrgID)
|
||||
require.Equal(c.t, role, s.OrgRole) // make sure the role was set properly
|
||||
|
||||
config := &rest.Config{
|
||||
Host: baseUrl,
|
||||
Username: s.Login,
|
||||
Password: key,
|
||||
}
|
||||
|
||||
client, err := dynamic.NewForConfig(config)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
return User{
|
||||
Identity: s,
|
||||
Client: client,
|
||||
password: key,
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
package playlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/playlist"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
)
|
||||
|
||||
@ -15,31 +19,176 @@ func TestPlaylist(t *testing.T) {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
helper := apis.NewK8sTestHelper(t)
|
||||
helper.SetGroupVersionResource(
|
||||
schema.GroupVersionResource{
|
||||
Group: "playlist.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "playlists",
|
||||
})
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: "playlist.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "playlists",
|
||||
}
|
||||
|
||||
t.Run("Check List from different org users", func(t *testing.T) {
|
||||
t.Run("Check direct List permissions from different org users", func(t *testing.T) {
|
||||
// Check view permissions
|
||||
rsp := helper.List(helper.Org1.Viewer, "default")
|
||||
rsp := helper.List(helper.Org1.Viewer, "default", gvr)
|
||||
require.Equal(t, 200, rsp.Response.StatusCode)
|
||||
require.NotNil(t, rsp.Result)
|
||||
require.Empty(t, rsp.Result.Items)
|
||||
require.Nil(t, rsp.Status)
|
||||
|
||||
// Check view permissions
|
||||
rsp = helper.List(helper.Org2.Viewer, "default")
|
||||
rsp = helper.List(helper.Org2.Viewer, "default", gvr)
|
||||
require.Equal(t, 403, rsp.Response.StatusCode) // Org2 can not see default namespace
|
||||
require.Nil(t, rsp.Result)
|
||||
require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason)
|
||||
|
||||
// Check view permissions
|
||||
rsp = helper.List(helper.Org2.Viewer, "org-22")
|
||||
rsp = helper.List(helper.Org2.Viewer, "org-22", gvr)
|
||||
require.Equal(t, 403, rsp.Response.StatusCode) // Unknown/not a member
|
||||
require.Nil(t, rsp.Result)
|
||||
require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason)
|
||||
})
|
||||
|
||||
t.Run("Check k8s client-go List from different org users", func(t *testing.T) {
|
||||
// Check Org1 Viewer
|
||||
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Viewer,
|
||||
Namespace: "", // << fills in the value org1 is allowed to see!
|
||||
GVR: gvr,
|
||||
})
|
||||
rsp, err := client.Resource.List(context.Background(), metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, rsp.Items)
|
||||
|
||||
// Check org2 viewer can not see org1 (default namespace)
|
||||
client = helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org2.Viewer,
|
||||
Namespace: "default", // actually org1
|
||||
GVR: gvr,
|
||||
})
|
||||
rsp, err = client.Resource.List(context.Background(), metav1.ListOptions{})
|
||||
statusError := helper.AsStatusError(err)
|
||||
require.Nil(t, rsp)
|
||||
require.Equal(t, metav1.StatusReasonForbidden, statusError.Status().Reason)
|
||||
|
||||
// Check invalid namespace
|
||||
client = helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org2.Viewer,
|
||||
Namespace: "org-22", // org 22 does not exist
|
||||
GVR: gvr,
|
||||
})
|
||||
rsp, err = client.Resource.List(context.Background(), metav1.ListOptions{})
|
||||
statusError = helper.AsStatusError(err)
|
||||
require.Nil(t, rsp)
|
||||
require.Equal(t, metav1.StatusReasonForbidden, statusError.Status().Reason)
|
||||
})
|
||||
|
||||
t.Run("Check playlist CRUD in legacy API appears in k8s apis", func(t *testing.T) {
|
||||
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Editor,
|
||||
GVR: gvr,
|
||||
})
|
||||
|
||||
// This includes the raw dashboard values that are currently sent (but should not be and are ignored)
|
||||
legacyPayload := `{
|
||||
"name": "Test",
|
||||
"interval": "20s",
|
||||
"items": [
|
||||
{
|
||||
"type": "dashboard_by_uid",
|
||||
"value": "xCmMwXdVz",
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "The dashboard",
|
||||
"kind": "dashboard",
|
||||
"uid": "xCmMwXdVz",
|
||||
"url": "/d/xCmMwXdVz/barchart-label-rotation-and-skipping",
|
||||
"tags": ["barchart", "gdev", "graph-ng", "panel-tests"],
|
||||
"location": "d1de6240-fd2e-4e13-99b6-f9d0c6b0550d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "graph-ng",
|
||||
"dashboards": [ "..." ]
|
||||
}
|
||||
],
|
||||
"uid": ""
|
||||
}`
|
||||
legacyCreate := apis.DoRequest(helper, apis.RequestParams{
|
||||
User: client.Args.User,
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/playlists",
|
||||
Body: []byte(legacyPayload),
|
||||
}, &playlist.Playlist{})
|
||||
require.NotNil(t, legacyCreate.Result)
|
||||
uid := legacyCreate.Result.UID
|
||||
require.NotEmpty(t, uid)
|
||||
|
||||
expectedResult := `{
|
||||
"apiVersion": "playlist.grafana.app/v0alpha1",
|
||||
"kind": "Playlist",
|
||||
"metadata": {
|
||||
"creationTimestamp": "${creationTimestamp}",
|
||||
"name": "` + uid + `",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "${resourceVersion}",
|
||||
"uid": "${uid}"
|
||||
},
|
||||
"spec": {
|
||||
"title": "Test",
|
||||
"interval": "20s",
|
||||
"items": [
|
||||
{
|
||||
"type": "dashboard_by_uid",
|
||||
"value": "xCmMwXdVz"
|
||||
},
|
||||
{
|
||||
"type": "dashboard_by_tag",
|
||||
"value": "graph-ng"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
// List includes the expected result
|
||||
k8sList, err := client.Resource.List(context.Background(), metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(k8sList.Items))
|
||||
require.JSONEq(t, expectedResult, client.SanitizeJSON(&k8sList.Items[0]))
|
||||
|
||||
// Get should return the same result
|
||||
found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, expectedResult, client.SanitizeJSON(found))
|
||||
|
||||
// Now modify the interval
|
||||
updatedInterval := `"interval": "10m"`
|
||||
legacyPayload = strings.Replace(legacyPayload, `"interval": "20s"`, updatedInterval, 1)
|
||||
expectedResult = strings.Replace(expectedResult, `"interval": "20s"`, updatedInterval, 1)
|
||||
dtoResponse := apis.DoRequest(helper, apis.RequestParams{
|
||||
User: client.Args.User,
|
||||
Method: http.MethodPut,
|
||||
Path: "/api/playlists/" + uid,
|
||||
Body: []byte(legacyPayload),
|
||||
}, &playlist.PlaylistDTO{})
|
||||
require.Equal(t, uid, dtoResponse.Result.Uid)
|
||||
require.Equal(t, "10m", dtoResponse.Result.Interval)
|
||||
|
||||
// Make sure the changed interval is now returned from k8s
|
||||
found, err = client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, expectedResult, client.SanitizeJSON(found))
|
||||
|
||||
// Delete does not return anything
|
||||
_ = apis.DoRequest(helper, apis.RequestParams{
|
||||
User: client.Args.User,
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/playlists/" + uid,
|
||||
Body: []byte(legacyPayload),
|
||||
}, &playlist.PlaylistDTO{}) // response is empty
|
||||
|
||||
found, err = client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
||||
statusError := helper.AsStatusError(err)
|
||||
require.Nil(t, found)
|
||||
require.Equal(t, metav1.StatusReasonNotFound, statusError.Status().Reason)
|
||||
})
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type AnyResource struct {
|
||||
@ -24,22 +19,3 @@ type AnyResourceList struct {
|
||||
|
||||
Items []map[string]any `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// Read local JSON or YAML file into a resource
|
||||
func (c *K8sTestHelper) LoadAnyResource(fpath string) AnyResource {
|
||||
c.t.Helper()
|
||||
|
||||
//nolint:gosec
|
||||
raw, err := os.ReadFile(fpath)
|
||||
require.NoError(c.t, err)
|
||||
require.NotEmpty(c.t, raw)
|
||||
|
||||
res := &AnyResource{}
|
||||
if json.Valid(raw) {
|
||||
err = json.Unmarshal(raw, res)
|
||||
} else {
|
||||
err = yaml.Unmarshal(raw, res)
|
||||
}
|
||||
require.NoError(c.t, err)
|
||||
return *res
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user