package dashboard import ( "context" "github.com/google/uuid" "github.com/coder/coder/v2/codersdk" ) // DefaultActions is a table of actions to perform. // D&D nerds will feel right at home here :-) // Note that the order of the table is important! // Entries must be in ascending order. var DefaultActions RollTable = []RollTableEntry{ {0, fetchWorkspaces, "fetch workspaces"}, {1, fetchUsers, "fetch users"}, {2, fetchTemplates, "fetch templates"}, {3, authCheckAsOwner, "authcheck owner"}, {4, authCheckAsNonOwner, "authcheck not owner"}, {5, fetchAuditLog, "fetch audit log"}, {6, fetchActiveUsers, "fetch active users"}, {7, fetchSuspendedUsers, "fetch suspended users"}, {8, fetchTemplateVersion, "fetch template version"}, {9, fetchWorkspace, "fetch workspace"}, {10, fetchTemplate, "fetch template"}, {11, fetchUserByID, "fetch user by ID"}, {12, fetchUserByUsername, "fetch user by username"}, {13, fetchWorkspaceBuild, "fetch workspace build"}, {14, fetchDeploymentConfig, "fetch deployment config"}, {15, fetchWorkspaceQuotaForUser, "fetch workspace quota for user"}, {16, fetchDeploymentStats, "fetch deployment stats"}, {17, fetchWorkspaceLogs, "fetch workspace logs"}, } // RollTable is a slice of rollTableEntry. type RollTable []RollTableEntry // RollTableEntry is an entry in the roll table. type RollTableEntry struct { // Roll is the minimum number required to perform the action. Roll int // Fn is the function to call. Fn func(ctx context.Context, p *Params) error // Label is used for logging. Label string } // choose returns the first entry in the table that is greater than or equal to n. func (r RollTable) choose(n int) RollTableEntry { for _, entry := range r { if entry.Roll >= n { return entry } } return RollTableEntry{} } // max returns the maximum roll in the table. // Important: this assumes that the table is sorted in ascending order. func (r RollTable) max() int { return r[len(r)-1].Roll } // Params is a set of parameters to pass to the actions in a rollTable. type Params struct { // client is the client to use for performing the action. client *codersdk.Client // me is the currently authenticated user. Lots of actions require this. me codersdk.User // For picking random resource IDs, we need to know what resources are // present. We store them in a cache to avoid fetching them every time. // This may seem counter-intuitive for load testing, but we want to avoid // muddying results. c *cache } // fetchWorkspaces fetches all workspaces. func fetchWorkspaces(ctx context.Context, p *Params) error { ws, err := p.client.Workspaces(ctx, codersdk.WorkspaceFilter{}) if err != nil { // store the workspaces for later use in case they change p.c.setWorkspaces(ws.Workspaces) } return err } // fetchUsers fetches all users. func fetchUsers(ctx context.Context, p *Params) error { users, err := p.client.Users(ctx, codersdk.UsersRequest{}) if err != nil { p.c.setUsers(users.Users) } return err } // fetchActiveUsers fetches all active users func fetchActiveUsers(ctx context.Context, p *Params) error { _, err := p.client.Users(ctx, codersdk.UsersRequest{ Status: codersdk.UserStatusActive, }) return err } // fetchSuspendedUsers fetches all suspended users func fetchSuspendedUsers(ctx context.Context, p *Params) error { _, err := p.client.Users(ctx, codersdk.UsersRequest{ Status: codersdk.UserStatusSuspended, }) return err } // fetchTemplates fetches all templates. func fetchTemplates(ctx context.Context, p *Params) error { templates, err := p.client.TemplatesByOrganization(ctx, p.me.OrganizationIDs[0]) if err != nil { p.c.setTemplates(templates) } return err } // fetchTemplateBuild fetches a single template version at random. func fetchTemplateVersion(ctx context.Context, p *Params) error { t := p.c.randTemplate() _, err := p.client.TemplateVersion(ctx, t.ActiveVersionID) return err } // fetchWorkspace fetches a single workspace at random. func fetchWorkspace(ctx context.Context, p *Params) error { w := p.c.randWorkspace() _, err := p.client.WorkspaceByOwnerAndName(ctx, w.OwnerName, w.Name, codersdk.WorkspaceOptions{}) return err } // fetchWorkspaceBuild fetches a single workspace build at random. func fetchWorkspaceBuild(ctx context.Context, p *Params) error { w := p.c.randWorkspace() _, err := p.client.WorkspaceBuild(ctx, w.LatestBuild.ID) return err } // fetchTemplate fetches a single template at random. func fetchTemplate(ctx context.Context, p *Params) error { t := p.c.randTemplate() _, err := p.client.Template(ctx, t.ID) return err } // fetchUserByID fetches a single user at random by ID. func fetchUserByID(ctx context.Context, p *Params) error { u := p.c.randUser() _, err := p.client.User(ctx, u.ID.String()) return err } // fetchUserByUsername fetches a single user at random by username. func fetchUserByUsername(ctx context.Context, p *Params) error { u := p.c.randUser() _, err := p.client.User(ctx, u.Username) return err } // fetchDeploymentConfig fetches the deployment config. func fetchDeploymentConfig(ctx context.Context, p *Params) error { _, err := p.client.DeploymentConfig(ctx) return err } // fetchWorkspaceQuotaForUser fetches the workspace quota for a random user. func fetchWorkspaceQuotaForUser(ctx context.Context, p *Params) error { u := p.c.randUser() _, err := p.client.WorkspaceQuota(ctx, u.ID.String()) return err } // fetchDeploymentStats fetches the deployment stats. func fetchDeploymentStats(ctx context.Context, p *Params) error { _, err := p.client.DeploymentStats(ctx) return err } // fetchWorkspaceLogs fetches the logs for a random workspace. func fetchWorkspaceLogs(ctx context.Context, p *Params) error { w := p.c.randWorkspace() ch, closer, err := p.client.WorkspaceBuildLogsAfter(ctx, w.LatestBuild.ID, 0) if err != nil { return err } defer func() { _ = closer.Close() }() // Drain the channel. for { select { case <-ctx.Done(): return ctx.Err() case l, ok := <-ch: if !ok { return nil } _ = l } } } // fetchAuditLog fetches the audit log. // As not all users have access to the audit log, we check first. func fetchAuditLog(ctx context.Context, p *Params) error { res, err := p.client.AuthCheck(ctx, codersdk.AuthorizationRequest{ Checks: map[string]codersdk.AuthorizationCheck{ "auditlog": { Object: codersdk.AuthorizationObject{ ResourceType: codersdk.ResourceAuditLog, }, Action: codersdk.ActionRead, }, }, }) if err != nil { return err } if !res["auditlog"] { return nil // we are not authorized to read the audit log } // Fetch the first 25 audit log entries. _, err = p.client.AuditLogs(ctx, codersdk.AuditLogsRequest{ Pagination: codersdk.Pagination{ Offset: 0, Limit: 25, }, }) return err } // authCheckAsOwner performs an auth check as the owner of a random // resource type and action. func authCheckAsOwner(ctx context.Context, p *Params) error { _, err := p.client.AuthCheck(ctx, randAuthReq( ownedBy(p.me.ID), withAction(randAction()), withObjType(randObjectType()), inOrg(p.me.OrganizationIDs[0]), )) return err } // authCheckAsNonOwner performs an auth check as a non-owner of a random // resource type and action. func authCheckAsNonOwner(ctx context.Context, p *Params) error { _, err := p.client.AuthCheck(ctx, randAuthReq( ownedBy(uuid.New()), withAction(randAction()), withObjType(randObjectType()), inOrg(p.me.OrganizationIDs[0]), )) return err } // nolint: gosec func randAuthReq(mut ...func(*codersdk.AuthorizationCheck)) codersdk.AuthorizationRequest { var check codersdk.AuthorizationCheck for _, m := range mut { m(&check) } return codersdk.AuthorizationRequest{ Checks: map[string]codersdk.AuthorizationCheck{ "check": check, }, } } func ownedBy(myID uuid.UUID) func(check *codersdk.AuthorizationCheck) { return func(check *codersdk.AuthorizationCheck) { check.Object.OwnerID = myID.String() } } func inOrg(orgID uuid.UUID) func(check *codersdk.AuthorizationCheck) { return func(check *codersdk.AuthorizationCheck) { check.Object.OrganizationID = orgID.String() } } func withObjType(objType codersdk.RBACResource) func(check *codersdk.AuthorizationCheck) { return func(check *codersdk.AuthorizationCheck) { check.Object.ResourceType = objType } } func withAction(action string) func(check *codersdk.AuthorizationCheck) { return func(check *codersdk.AuthorizationCheck) { check.Action = action } } func randAction() string { return pick(codersdk.AllRBACActions) } func randObjectType() codersdk.RBACResource { return pick(codersdk.AllRBACResources) }