The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/tests/apis/helper.go

915 lines
26 KiB

package apis
import (
"bytes"
"context"
"encoding/json"
goerrors "errors"
"fmt"
"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"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/options"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"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"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
const (
Org1 = "Org1"
Org2 = "OrgB"
)
type K8sTestHelper struct {
t *testing.T
env server.TestEnv
Namespacer request.NamespaceMapper
Org1 OrgUsers // default
OrgB OrgUsers // some other id
// // Registered groups
groups []metav1.APIGroup
orgSvc org.Service
teamSvc team.Service
userSvc user.Service
}
func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper {
t.Helper()
// Use GRPC server when not configured
if opts.APIServerStorageType == "" && opts.GRPCServerAddress == "" {
// TODO, this really should be gRPC, but sometimes fails in drone
// the two *should* be identical, but we have seen issues when using real gRPC vs channel
opts.APIServerStorageType = options.StorageTypeUnified // TODO, should be GRPC
}
// Always enable `FlagAppPlatformGrpcClientAuth` for k8s integration tests, as this is the desired behavior.
// The flag only exists to support the transition from the old to the new behavior in dev/ops/prod.
opts.EnableFeatureToggles = append(opts.EnableFeatureToggles, featuremgmt.FlagAppPlatformGrpcClientAuth)
dir, path := testinfra.CreateGrafDir(t, opts)
_, env := testinfra.StartGrafanaEnv(t, dir, path)
c := &K8sTestHelper{
env: *env,
t: t,
Namespacer: request.GetNamespaceMapper(nil),
}
quotaService := quotaimpl.ProvideService(c.env.SQLStore, c.env.Cfg)
orgSvc, err := orgimpl.ProvideService(c.env.SQLStore, c.env.Cfg, quotaService)
require.NoError(c.t, err)
c.orgSvc = orgSvc
teamSvc, err := teamimpl.ProvideService(c.env.SQLStore, c.env.Cfg, tracing.NewNoopTracerService())
require.NoError(c.t, err)
c.teamSvc = teamSvc
userSvc, err := userimpl.ProvideService(
c.env.SQLStore, orgSvc, c.env.Cfg, teamSvc,
localcache.ProvideService(), tracing.NewNoopTracerService(), quotaService,
supportbundlestest.NewFakeBundleService())
require.NoError(c.t, err)
c.userSvc = userSvc
_ = c.CreateOrg(Org1)
_ = c.CreateOrg(Org2)
c.Org1 = c.createTestUsers(Org1)
c.OrgB = c.createTestUsers(Org2)
c.loadAPIGroups()
// ensure unified storage is alive and running
ctx := identity.WithRequester(context.Background(), c.Org1.Admin.Identity)
rsp, err := c.env.ResourceClient.IsHealthy(ctx, &resourcepb.HealthCheckRequest{})
require.NoError(t, err, "unable to read resource client health check")
require.Equal(t, resourcepb.HealthCheckResponse_SERVING, rsp.Status)
return c
}
func (c *K8sTestHelper) loadAPIGroups() {
for {
rsp := DoRequest(c, RequestParams{
User: c.Org1.Viewer,
Path: "/apis",
}, &metav1.APIGroupList{})
if rsp.Response.StatusCode == http.StatusOK {
c.groups = rsp.Result.Groups
return
}
time.Sleep(100 * time.Millisecond)
}
}
func (c *K8sTestHelper) GetEnv() server.TestEnv {
return c.env
}
func (c *K8sTestHelper) Shutdown() {
err := c.env.Server.Shutdown(context.Background(), "done")
require.NoError(c.t, err)
}
type ResourceClientArgs struct {
// 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 {
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()
// Validate that either User or ServiceAccountToken is provided, but not both
err := args.Validate()
require.NoError(c.t, err)
if args.Namespace == "" {
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")
}
}
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,
Args: args,
Resource: 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
}
func (c *K8sTestHelper) EnsureStatusError(err error, expectedHttpStatus int, expectedMessage string) {
statusError := c.AsStatusError(err)
require.NotNil(c.t, statusError)
require.Equal(c.t, int32(expectedHttpStatus), statusError.ErrStatus.Code)
if expectedMessage != "" {
require.Equal(c.t, expectedMessage, statusError.ErrStatus.Message)
}
}
func (c *K8sResourceClient) SanitizeJSONList(v *unstructured.UnstructuredList, replaceMeta ...string) string {
c.t.Helper()
clean := &unstructured.UnstructuredList{}
for _, item := range v.Items {
copy := c.sanitizeObject(&item, replaceMeta...)
clean.Items = append(clean.Items, *copy)
}
out, err := json.MarshalIndent(clean, "", " ")
require.NoError(c.t, err)
return string(out)
}
func (c *K8sResourceClient) SpecJSON(v *unstructured.UnstructuredList) string {
c.t.Helper()
clean := []any{}
for _, item := range v.Items {
clean = append(clean, item.Object["spec"])
}
out, err := json.MarshalIndent(clean, "", " ")
require.NoError(c.t, err)
return string(out)
}
// remove the meta keys that are expected to change each time
func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured, replaceMeta ...string) string {
c.t.Helper()
copy := c.sanitizeObject(v, replaceMeta...)
out, err := json.MarshalIndent(copy, "", " ")
require.NoError(c.t, err)
return string(out)
}
// remove the meta keys that are expected to change each time
func (c *K8sResourceClient) sanitizeObject(v *unstructured.Unstructured, replaceMeta ...string) *unstructured.Unstructured {
c.t.Helper()
deep := v.DeepCopy()
deep.SetAnnotations(nil)
deep.SetManagedFields(nil)
copy := deep.Object
meta, ok := copy["metadata"].(map[string]any)
require.True(c.t, ok)
// remove generation
delete(meta, "generation")
replaceMeta = append(replaceMeta, "creationTimestamp", "resourceVersion", "uid")
for _, key := range replaceMeta {
if key == "labels" {
delete(meta, key)
continue
}
old, ok := meta[key]
if ok {
require.NotEmpty(c.t, old)
meta[key] = fmt.Sprintf("${%s}", key)
}
}
deep.Object["metadata"] = meta
return deep
}
type OrgUsers struct {
Admin User
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
}
type User struct {
Identity identity.Requester
password string
baseURL string
}
func (c *User) NewRestConfig() *rest.Config {
return &rest.Config{
Host: c.baseURL,
Username: c.Identity.GetLogin(),
Password: c.password,
}
}
// Implements: apiserver.RestConfigProvider
func (c *User) GetRestConfig(context.Context) (*rest.Config, error) {
return c.NewRestConfig(), nil
}
func (c *User) ResourceClient(t *testing.T, gvr schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
client, err := dynamic.NewForConfig(c.NewRestConfig())
require.NoError(t, err)
return client.Resource(gvr)
}
func (c *User) RESTClient(t *testing.T, gv *schema.GroupVersion) *rest.RESTClient {
cfg := dynamic.ConfigFor(c.NewRestConfig()) // adds negotiated serializers!
cfg.GroupVersion = gv
cfg.APIPath = "apis" // the plural
client, err := rest.RESTClientFor(cfg)
require.NoError(t, err)
return client
}
type RequestParams struct {
User User
Method string // GET, POST, PATCH, etc
Path string
Body []byte
ContentType string
Accept string
}
type K8sResponse[T any] struct {
Response *http.Response
Body []byte
Result *T
Status *metav1.Status
}
type AnyResourceResponse = K8sResponse[AnyResource]
type AnyResourceListResponse = K8sResponse[AnyResourceList]
func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyResource) AnyResourceResponse {
c.t.Helper()
namespace := payload.Namespace
if namespace == "" {
namespace = c.Namespacer(user.Identity.GetOrgID())
}
path := fmt.Sprintf("/apis/%s/namespaces/%s/%s",
payload.APIVersion, namespace, resource)
if payload.Name != "" {
path = fmt.Sprintf("%s/%s", path, payload.Name)
}
body, err := json.Marshal(payload)
require.NoError(c.t, err)
return DoRequest(c, RequestParams{
Method: http.MethodPost,
Path: path,
User: user,
Body: body,
}, &AnyResource{})
}
func (c *K8sTestHelper) PutResource(user User, resource string, payload AnyResource) AnyResourceResponse {
c.t.Helper()
path := fmt.Sprintf("/apis/%s/namespaces/%s/%s/%s",
payload.APIVersion, payload.Namespace, resource, payload.Name)
body, err := json.Marshal(payload)
require.NoError(c.t, err)
return DoRequest(c, RequestParams{
Method: http.MethodPut,
Path: path,
User: user,
Body: body,
}, &AnyResource{})
}
func (c *K8sTestHelper) List(user User, namespace string, gvr schema.GroupVersionResource) AnyResourceListResponse {
c.t.Helper()
return DoRequest(c, RequestParams{
User: user,
Path: fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s",
gvr.Group,
gvr.Version,
namespace,
gvr.Resource),
}, &AnyResourceList{})
}
func DoRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResponse[T] {
c.t.Helper()
if params.Method == "" {
params.Method = http.MethodGet
}
// Get the URL
addr := c.env.Server.HTTPServer.Listener.Addr()
baseUrl := fmt.Sprintf("http://%s", addr)
login := params.User.Identity.GetLogin()
if login != "" && params.User.password != "" {
baseUrl = fmt.Sprintf("http://%s:%s@%s", login, params.User.password, addr)
}
contentType := params.ContentType
var body io.Reader
if params.Body != nil {
body = bytes.NewReader(params.Body)
if contentType == "" && json.Valid(params.Body) {
contentType = "application/json"
}
}
req, err := http.NewRequest(params.Method, fmt.Sprintf(
"%s%s",
baseUrl,
params.Path,
), body)
require.NoError(c.t, err)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if params.Accept != "" {
req.Header.Set("Accept", params.Accept)
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
rsp, err := client.Do(req)
require.NoError(c.t, err)
r := K8sResponse[T]{
Response: rsp,
Result: result,
}
defer func() {
_ = rsp.Body.Close() // ignore any close errors
}()
r.Body, _ = io.ReadAll(rsp.Body)
if json.Valid(r.Body) {
_ = json.Unmarshal(r.Body, r.Result)
s := &metav1.Status{}
err := json.Unmarshal(r.Body, s)
if err == nil && s.Kind == "Status" { // Usually an error!
r.Status = s
r.Result = nil
}
}
return r
}
// Read local JSON or YAML file into a resource
func (c *K8sTestHelper) LoadYAMLOrJSONFile(fpath string) *unstructured.Unstructured {
c.t.Helper()
return c.LoadYAMLOrJSON(string(c.LoadFile(fpath)))
}
// Read local file into a byte slice. Does not need to be a resource.
func (c *K8sTestHelper) LoadFile(fpath string) []byte {
c.t.Helper()
//nolint:gosec
raw, err := os.ReadFile(fpath)
require.NoError(c.t, err)
require.NotEmpty(c.t, raw)
return 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(orgName string) OrgUsers {
c.t.Helper()
users := OrgUsers{
Admin: c.CreateUser("admin", orgName, org.RoleAdmin, nil),
Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil),
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.
c.AddOrUpdateTeamMember(users.Admin, users.Staff.ID, team.PermissionTypeAdmin)
c.AddOrUpdateTeamMember(users.Editor, users.Staff.ID, team.PermissionTypeMember)
return users
}
func (c *K8sTestHelper) CreateOrg(name string) int64 {
if name == Org1 {
return 1
}
oldAssing := c.env.Cfg.AutoAssignOrg
defer func() {
c.env.Cfg.AutoAssignOrg = oldAssing
}()
c.env.Cfg.AutoAssignOrg = false
o, err := c.orgSvc.GetByName(context.Background(), &org.GetOrgByNameQuery{
Name: name,
})
if goerrors.Is(err, org.ErrOrgNotFound) {
id, err := c.orgSvc.GetOrCreate(context.Background(), name)
require.NoError(c.t, err)
return id
}
require.NoError(c.t, err)
return o.ID
}
func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.RoleType, permissions []resourcepermissions.SetResourcePermissionCommand) User {
c.t.Helper()
orgId := c.CreateOrg(orgName)
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
// make org1 admins grafana admins
isGrafanaAdmin := basicRole == identity.RoleAdmin && orgId == 1
u, err := c.userSvc.Create(context.Background(), &user.CreateUserCommand{
DefaultOrgRole: string(basicRole),
Password: user.Password(name),
Login: fmt.Sprintf("%s-%d", name, orgId),
OrgID: orgId,
IsAdmin: isGrafanaAdmin,
})
// for tests to work we need to add grafana admins to every org
if isGrafanaAdmin {
orgs, err := c.orgSvc.Search(context.Background(), &org.SearchOrgsQuery{})
require.NoError(c.t, err)
for _, o := range orgs {
_ = c.orgSvc.AddOrgUser(context.Background(), &org.AddOrgUserCommand{
Role: identity.RoleAdmin,
OrgID: o.ID,
UserID: u.ID,
})
}
}
require.NoError(c.t, err)
require.Equal(c.t, orgId, u.OrgID)
require.True(c.t, u.ID > 0)
// should this always return a user with ID token?
s, err := c.userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
UserID: u.ID,
Login: u.Login,
Email: u.Email,
OrgID: orgId,
})
require.NoError(c.t, err)
require.Equal(c.t, orgId, s.OrgID)
require.Equal(c.t, basicRole, s.OrgRole) // make sure the role was set properly
idToken, idClaims, err := c.env.IDService.SignIdentity(context.Background(), s)
require.NoError(c.t, err)
s.IDToken = idToken
s.IDTokenClaims = idClaims
usr := User{
Identity: s,
password: name,
baseURL: baseUrl,
}
if len(permissions) > 0 {
c.SetPermissions(usr, permissions)
}
return usr
}
func (c *K8sTestHelper) SetPermissions(user User, permissions []resourcepermissions.SetResourcePermissionCommand) {
// nolint:staticcheck
id, err := user.Identity.GetInternalID()
require.NoError(c.t, err)
permissionsStore := resourcepermissions.NewStore(c.env.Cfg, c.env.SQLStore, featuremgmt.WithFeatures())
for _, permission := range permissions {
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
user.Identity.GetOrgID(),
accesscontrol.User{ID: id},
permission, nil)
require.NoError(c.t, err)
}
}
func (c *K8sTestHelper) AddOrUpdateTeamMember(user User, teamID int64, permission team.PermissionType) {
teampermissionSvc, err := ossaccesscontrol.ProvideTeamPermissions(
c.env.Cfg,
c.env.FeatureToggles,
c.env.Server.HTTPServer.RouteRegister,
c.env.SQLStore,
c.env.Server.HTTPServer.AccessControl,
c.env.Server.HTTPServer.License,
c.env.Server.HTTPServer.AlertNG.AccesscontrolService,
c.teamSvc,
c.userSvc,
resourcepermissions.NewActionSetService(),
)
require.NoError(c.t, err)
id, err := user.Identity.GetInternalID()
require.NoError(c.t, err)
teamIDString := strconv.FormatInt(teamID, 10)
_, err = teampermissionSvc.SetUserPermission(context.Background(), user.Identity.GetOrgID(), accesscontrol.User{ID: id}, teamIDString, permission.String())
require.NoError(c.t, err)
}
func (c *K8sTestHelper) NewDiscoveryClient() *discovery.DiscoveryClient {
c.t.Helper()
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
conf := &rest.Config{
Host: baseUrl,
Username: c.Org1.Admin.Identity.GetLogin(),
Password: c.Org1.Admin.password,
}
client, err := discovery.NewDiscoveryClientForConfig(conf)
require.NoError(c.t, err)
return client
}
func (c *K8sTestHelper) GetGroupVersionInfoJSON(group string) string {
c.t.Helper()
disco := c.NewDiscoveryClient()
req := disco.RESTClient().Get().
Prefix("apis").
SetHeader("Accept", "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json")
result := req.Do(context.Background())
require.NoError(c.t, result.Error())
type DiscoItem struct {
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
Versions []any `json:"versions,omitempty"`
}
type DiscoList struct {
Items []DiscoItem `json:"items"`
}
raw, err := result.Raw()
require.NoError(c.t, err)
all := &DiscoList{}
err = json.Unmarshal(raw, all)
require.NoError(c.t, err)
for _, item := range all.Items {
if item.Metadata.Name == group {
v, err := json.MarshalIndent(item.Versions, "", " ")
require.NoError(c.t, err)
return string(v)
}
}
require.Failf(c.t, "could not find discovery info for: %s", group)
return ""
}
func (c *K8sTestHelper) CreateDS(cmd *datasources.AddDataSourceCommand) *datasources.DataSource {
c.t.Helper()
dataSource, err := c.env.Server.HTTPServer.DataSourcesService.AddDataSource(context.Background(), cmd)
require.NoError(c.t, err)
return dataSource
}
func (c *K8sTestHelper) CreateTeam(name, email string, orgID int64) team.Team {
c.t.Helper()
teamCmd := team.CreateTeamCommand{
Name: name,
Email: email,
OrgID: orgID,
}
team, err := c.teamSvc.CreateTeam(context.Background(), &teamCmd)
require.NoError(c.t, err)
return team
}
// 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{})
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())
}
}
})
}
// 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))
}
// Ensures that the passed error is an APIStatus error and fails the test if it is not.
func (c *K8sTestHelper) RequireApiErrorStatus(err error, reason metav1.StatusReason, httpCode int) metav1.Status {
require.Error(c.t, err)
status, ok := utils.ExtractApiErrorStatus(err)
if !ok {
c.t.Fatalf("Expected error to be an APIStatus, but got %T", err)
}
if reason != metav1.StatusReasonUnknown {
require.Equal(c.t, status.Reason, reason)
}
if httpCode != 0 {
require.Equal(c.t, status.Code, int32(httpCode))
}
return status
}