Dashboards: Add Dashboard API Validation tests and fix underlying issues (#103502)

This commit is contained in:
Marco de Abreu
2025-04-07 05:44:28 +03:00
committed by GitHub
parent 427715b070
commit b2f80d4dca
16 changed files with 1521 additions and 240 deletions
+147 -6
View File
@@ -41,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
@@ -153,9 +154,22 @@ func (c *K8sTestHelper) Shutdown() {
}
type ResourceClientArgs struct {
User User
Namespace string
GVR schema.GroupVersionResource
// Provide either a user or a service account token
User User
ServiceAccountToken string
Namespace string
GVR schema.GroupVersionResource
}
// Validate ensures that either User or ServiceAccountToken is provided, but not both
func (args ResourceClientArgs) Validate() error {
if (args.User != User{}) && args.ServiceAccountToken != "" {
return fmt.Errorf("cannot provide both User and ServiceAccountToken")
}
if (args.User == User{}) && args.ServiceAccountToken == "" {
return fmt.Errorf("must provide either User or ServiceAccountToken")
}
return nil
}
type K8sResourceClient struct {
@@ -168,12 +182,33 @@ type K8sResourceClient struct {
func (c *K8sTestHelper) GetResourceClient(args ResourceClientArgs) *K8sResourceClient {
c.t.Helper()
// Validate that either User or ServiceAccountToken is provided, but not both
err := args.Validate()
require.NoError(c.t, err)
if args.Namespace == "" {
args.Namespace = c.Namespacer(args.User.Identity.GetOrgID())
if args.User != (User{}) {
args.Namespace = c.Namespacer(args.User.Identity.GetOrgID())
} else {
// For service account token, we need to pass the namespace directly
require.NotEmpty(c.t, args.Namespace, "Namespace must be provided when using ServiceAccountToken")
}
}
client, err := dynamic.NewForConfig(args.User.NewRestConfig())
require.NoError(c.t, err)
var client dynamic.Interface
var clientErr error
if args.User != (User{}) {
client, clientErr = dynamic.NewForConfig(args.User.NewRestConfig())
} else {
// Use service account token for authentication
cfg := &rest.Config{
Host: fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr()),
BearerToken: args.ServiceAccountToken,
}
client, clientErr = dynamic.NewForConfig(cfg)
}
require.NoError(c.t, clientErr)
return &K8sResourceClient{
t: c.t,
@@ -269,6 +304,14 @@ type OrgUsers struct {
Editor User
Viewer User
// Separate standalone service accounts with different roles
AdminServiceAccount serviceaccounts.ServiceAccountDTO
AdminServiceAccountToken string
EditorServiceAccount serviceaccounts.ServiceAccountDTO
EditorServiceAccountToken string
ViewerServiceAccount serviceaccounts.ServiceAccountDTO
ViewerServiceAccountToken string
// The team with admin+editor in it (but not viewer)
Staff team.Team
}
@@ -488,6 +531,16 @@ func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers {
Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil),
}
// Create service accounts
users.AdminServiceAccount = c.CreateServiceAccount(users.Admin, "admin-sa", users.Admin.Identity.GetOrgID(), org.RoleAdmin)
users.AdminServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.AdminServiceAccount.Id, users.Admin.Identity.GetOrgID(), "admin-token", 0)
users.EditorServiceAccount = c.CreateServiceAccount(users.Admin, "editor-sa", users.Admin.Identity.GetOrgID(), org.RoleEditor)
users.EditorServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.EditorServiceAccount.Id, users.Admin.Identity.GetOrgID(), "editor-token", 0)
users.ViewerServiceAccount = c.CreateServiceAccount(users.Admin, "viewer-sa", users.Admin.Identity.GetOrgID(), org.RoleViewer)
users.ViewerServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.ViewerServiceAccount.Id, users.Admin.Identity.GetOrgID(), "viewer-token", 0)
users.Staff = c.CreateTeam("staff", "staff@"+orgName, users.Admin.Identity.GetOrgID())
// Add Admin and Editor to Staff team as Admin and Member, respectively.
@@ -744,3 +797,91 @@ func VerifyOpenAPISnapshots(t *testing.T, dir string, gv schema.GroupVersion, h
}
})
}
// CreateServiceAccount creates a service account with the specified name, organization, and role using the HTTP API
func (c *K8sTestHelper) CreateServiceAccount(executingUser User, name string, orgID int64, role org.RoleType) serviceaccounts.ServiceAccountDTO {
c.t.Helper()
saForm := struct {
Name string `json:"name"`
Role org.RoleType `json:"role"`
IsDisabled bool `json:"isDisabled"`
}{
Name: name,
Role: role,
IsDisabled: false,
}
body, err := json.Marshal(saForm)
require.NoError(c.t, err)
resp := DoRequest(c, RequestParams{
User: executingUser,
Method: http.MethodPost,
Path: "/api/serviceaccounts/",
Body: body,
}, &serviceaccounts.ServiceAccountDTO{})
require.Equal(c.t, http.StatusCreated, resp.Response.StatusCode, "failed to create service account, body: %s", string(resp.Body))
require.NotNil(c.t, resp.Result, "failed to parse response body: %s", string(resp.Body))
return *resp.Result
}
// CreateServiceAccountToken creates a token for the specified service account using the HTTP API
func (c *K8sTestHelper) CreateServiceAccountToken(user User, saID int64, orgID int64, tokenName string, secondsToLive int64) string {
c.t.Helper()
tokenCmd := struct {
Name string `json:"name"`
SecondsToLive int64 `json:"secondsToLive"`
}{
Name: tokenName,
SecondsToLive: secondsToLive,
}
body, err := json.Marshal(tokenCmd)
require.NoError(c.t, err)
resp := DoRequest(c, RequestParams{
User: user,
Method: http.MethodPost,
Path: fmt.Sprintf("/api/serviceaccounts/%d/tokens", saID),
Body: body,
}, &struct {
ID int64 `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
}{})
require.Equal(c.t, http.StatusOK, resp.Response.StatusCode, "failed to create token, body: %s", string(resp.Body))
require.NotNil(c.t, resp.Result, "failed to parse response body: %s", string(resp.Body))
return resp.Result.Key
}
// DeleteServiceAccountToken deletes a token for the specified service account using the HTTP API
func (c *K8sTestHelper) DeleteServiceAccountToken(user User, orgID int64, saID int64, tokenID int64) {
c.t.Helper()
resp := DoRequest(c, RequestParams{
User: user,
Method: http.MethodDelete,
Path: fmt.Sprintf("/api/serviceaccounts/%d/tokens/%d", saID, tokenID),
}, &struct{}{})
require.Equal(c.t, http.StatusOK, resp.Response.StatusCode, "failed to delete token, body: %s", string(resp.Body))
}
// DeleteServiceAccount deletes a service account for the specified organization and ID using the HTTP API
func (c *K8sTestHelper) DeleteServiceAccount(user User, orgID int64, saID int64) {
c.t.Helper()
resp := DoRequest(c, RequestParams{
User: user,
Method: http.MethodDelete,
Path: fmt.Sprintf("/api/serviceaccounts/%d", saID),
}, &struct{}{})
require.Equal(c.t, http.StatusOK, resp.Response.StatusCode, "failed to delete service account, body: %s", string(resp.Body))
}