diff --git a/pkg/tests/apis/client.go b/pkg/tests/apis/client.go new file mode 100644 index 00000000000..f3acfa72a6d --- /dev/null +++ b/pkg/tests/apis/client.go @@ -0,0 +1,95 @@ +package apis + +import ( + "context" + + 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/types" + "k8s.io/client-go/dynamic" +) + +// TypedClient is the struct that implements a typed interface for resource operations +type TypedClient[T any, L any] struct { + Client dynamic.ResourceInterface +} + +func (c *TypedClient[T, L]) Create(ctx context.Context, resource *T, opts metav1.CreateOptions) (*T, error) { + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) + if err != nil { + return nil, err + } + u := &unstructured.Unstructured{Object: unstructuredObj} + result, err := c.Client.Create(ctx, u, opts) + if err != nil { + return nil, err + } + createdObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, createdObj) + if err != nil { + return nil, err + } + return createdObj, nil +} + +func (c *TypedClient[T, L]) Update(ctx context.Context, resource *T, opts metav1.UpdateOptions) (*T, error) { + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) + if err != nil { + return nil, err + } + u := &unstructured.Unstructured{Object: unstructuredObj} + result, err := c.Client.Update(ctx, u, opts) + if err != nil { + return nil, err + } + updatedObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, updatedObj) + if err != nil { + return nil, err + } + return updatedObj, nil +} + +func (c *TypedClient[T, L]) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return c.Client.Delete(ctx, name, opts) +} + +func (c *TypedClient[T, L]) Get(ctx context.Context, name string, opts metav1.GetOptions) (*T, error) { + result, err := c.Client.Get(ctx, name, opts) + if err != nil { + return nil, err + } + retrievedObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, retrievedObj) + if err != nil { + return nil, err + } + return retrievedObj, nil +} + +func (c *TypedClient[T, L]) List(ctx context.Context, opts metav1.ListOptions) (*L, error) { + result, err := c.Client.List(ctx, opts) + if err != nil { + return nil, err + } + listObj := new(L) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.UnstructuredContent(), listObj) + if err != nil { + return nil, err + } + return listObj, nil +} + +func (c *TypedClient[T, L]) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*T, error) { + result, err := c.Client.Patch(ctx, name, pt, data, opts, subresources...) + if err != nil { + return nil, err + } + patchedObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, patchedObj) + if err != nil { + return nil, err + } + return patchedObj, nil +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index efcd10c9a98..39cc4d8651e 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -8,10 +8,12 @@ import ( "io" "net/http" "os" + "path/filepath" "strconv" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,7 +21,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/yaml" - "k8s.io/apimachinery/pkg/types" yamlutil "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" @@ -661,86 +662,49 @@ func (c *K8sTestHelper) CreateTeam(name, email string, orgID int64) team.Team { return team } -// TypedClient is the struct that implements a typed interface for resource operations -type TypedClient[T any, L any] struct { - Client dynamic.ResourceInterface -} +// Compare the OpenAPI schema from one api against a cached snapshot +func VerifyOpenAPISnapshots(t *testing.T, dir string, gv schema.GroupVersion, h *K8sTestHelper) { + if gv.Group == "" { + return // skip invalid groups + } + path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) + t.Run(path, func(t *testing.T) { + rsp := DoRequest(h, RequestParams{ + Method: http.MethodGet, + Path: path, + User: h.Org1.Admin, + }, &AnyResource{}) -func (c *TypedClient[T, L]) Create(ctx context.Context, resource *T, opts metav1.CreateOptions) (*T, error) { - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) - if err != nil { - return nil, err - } - u := &unstructured.Unstructured{Object: unstructuredObj} - result, err := c.Client.Create(ctx, u, opts) - if err != nil { - return nil, err - } - createdObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, createdObj) - if err != nil { - return nil, err - } - return createdObj, nil -} + require.NotNil(t, rsp.Response) + require.Equal(t, 200, rsp.Response.StatusCode, path) -func (c *TypedClient[T, L]) Update(ctx context.Context, resource *T, opts metav1.UpdateOptions) (*T, error) { - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) - if err != nil { - return nil, err - } - u := &unstructured.Unstructured{Object: unstructuredObj} - result, err := c.Client.Update(ctx, u, opts) - if err != nil { - return nil, err - } - updatedObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, updatedObj) - if err != nil { - return nil, err - } - return updatedObj, nil -} + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, rsp.Body, "", " ") + require.NoError(t, err) + pretty := prettyJSON.String() -func (c *TypedClient[T, L]) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { - return c.Client.Delete(ctx, name, opts) -} + write := false + fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) -func (c *TypedClient[T, L]) Get(ctx context.Context, name string, opts metav1.GetOptions) (*T, error) { - result, err := c.Client.Get(ctx, name, opts) - if err != nil { - return nil, err - } - retrievedObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, retrievedObj) - if err != nil { - return nil, err - } - return retrievedObj, nil -} + // nolint:gosec + // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths + body, err := os.ReadFile(fpath) + if err == nil { + if !assert.JSONEq(t, string(body), pretty) { + t.Logf("openapi spec has changed: %s", path) + t.Fail() + write = true + } + } else { + t.Errorf("missing openapi spec for: %s", path) + write = true + } -func (c *TypedClient[T, L]) List(ctx context.Context, opts metav1.ListOptions) (*L, error) { - result, err := c.Client.List(ctx, opts) - if err != nil { - return nil, err - } - listObj := new(L) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.UnstructuredContent(), listObj) - if err != nil { - return nil, err - } - return listObj, nil -} - -func (c *TypedClient[T, L]) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*T, error) { - result, err := c.Client.Patch(ctx, name, pt, data, opts, subresources...) - if err != nil { - return nil, err - } - patchedObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, patchedObj) - if err != nil { - return nil, err - } - return patchedObj, nil + if write { + e2 := os.WriteFile(fpath, []byte(pretty), 0644) + if e2 != nil { + t.Errorf("error writing file: %s", e2.Error()) + } + } + }) } diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index a4a7164fa0d..403a543d544 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -1,22 +1,16 @@ package apis import ( - "bytes" "context" "encoding/json" "fmt" - "net/http" - "os" - "path/filepath" "testing" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/version" apimachineryversion "k8s.io/apimachinery/pkg/version" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -36,6 +30,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesFoldersServiceV2, // Will be default on by G12 featuremgmt.FlagQueryService, // Query Library + featuremgmt.FlagProvisioning, }, }) @@ -77,50 +72,3 @@ func TestIntegrationOpenAPIs(t *testing.T) { VerifyOpenAPISnapshots(t, dir, gv, h) } } - -// This function should be moved to oss (it is now a duplicate) -func VerifyOpenAPISnapshots(t *testing.T, dir string, gv schema.GroupVersion, h *K8sTestHelper) { - if gv.Group == "" { - return // skip invalid groups - } - path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) - t.Run(path, func(t *testing.T) { - rsp := DoRequest(h, RequestParams{ - Method: http.MethodGet, - Path: path, - User: h.Org1.Admin, - }, &AnyResource{}) - - require.NotNil(t, rsp.Response) - require.Equal(t, 200, rsp.Response.StatusCode, path) - - var prettyJSON bytes.Buffer - err := json.Indent(&prettyJSON, rsp.Body, "", " ") - require.NoError(t, err) - pretty := prettyJSON.String() - - write := false - fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) - - // nolint:gosec - // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths - body, err := os.ReadFile(fpath) - if err == nil { - if !assert.JSONEq(t, string(body), pretty) { - t.Logf("openapi spec has changed: %s", path) - t.Fail() - write = true - } - } else { - t.Errorf("missing openapi spec for: %s", path) - write = true - } - - if write { - e2 := os.WriteFile(fpath, []byte(pretty), 0644) - if e2 != nil { - t.Errorf("error writing file: %s", e2.Error()) - } - } - }) -} diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 6010c712356..eaeb4db521d 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -295,6 +295,13 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { _, err = alertingSect.NewKey("max_attempts", "3") require.NoError(t, err) + if opts.LicensePath != "" { + section, err := cfg.NewSection("enterprise") + require.NoError(t, err) + _, err = section.NewKey("license_path", opts.LicensePath) + require.NoError(t, err) + } + rbacSect, err := cfg.NewSection("rbac") require.NoError(t, err) _, err = rbacSect.NewKey("permission_cache", "false") @@ -529,6 +536,7 @@ type GrafanaOpts struct { GrafanaComAPIURL string UnifiedStorageConfig map[string]setting.UnifiedStorageConfig GrafanaComSSOAPIToken string + LicensePath string // When "unified-grpc" is selected it will also start the grpc server APIServerStorageType options.StorageType