fix: Remove unused workspace routes in favor of list with filter (#2038)

* fix: Remove unused workspace routes in favor of list with filter

This consolidates the workspace routes into a single place.
It allows users to fetch a workspace by their username and
workspace name, which will be used by the frontend for routing.

* Fix RBAC

* Fix CLI usages
This commit is contained in:
Kyle Carberry
2022-06-03 14:36:08 -05:00
committed by GitHub
parent d8c440188e
commit 43f622a52d
26 changed files with 72 additions and 424 deletions
+3 -15
View File
@@ -41,12 +41,8 @@ func autostartShow() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
@@ -93,10 +89,6 @@ func autostartEnable() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostartTimezone, autostartMinute, autostartHour, autostartDayOfWeek)
validSchedule, err := schedule.Weekly(spec)
@@ -104,7 +96,7 @@ func autostartEnable() *cobra.Command {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
@@ -142,12 +134,8 @@ func autostartDisable() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
+1 -6
View File
@@ -43,12 +43,7 @@ func bump() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current org: %w", err)
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
+3 -5
View File
@@ -48,10 +48,6 @@ func configSSH() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
if strings.HasPrefix(sshConfigFile, "~/") {
dirname, _ := os.UserHomeDir()
sshConfigFile = filepath.Join(dirname, sshConfigFile[2:])
@@ -65,7 +61,9 @@ func configSSH() *cobra.Command {
sshConfigContent = sshConfigContent[:startIndex-1] + sshConfigContent[endIndex+len(sshEndToken):]
}
workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me)
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return err
}
+2 -2
View File
@@ -49,7 +49,7 @@ func create() *cobra.Command {
workspaceName, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Specify a name for your workspace:",
Validate: func(workspaceName string) error {
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, workspaceName)
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName)
if err == nil {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
}
@@ -75,7 +75,7 @@ func create() *cobra.Command {
return xerrors.Errorf("TTL must be at least 1 minute")
}
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, workspaceName)
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName)
if err == nil {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
}
+1 -5
View File
@@ -30,11 +30,7 @@ func delete() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
+1 -5
View File
@@ -70,12 +70,8 @@ func portForward() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, agent, err := getWorkspaceAndAgent(cmd, client, organization.ID, codersdk.Me, args[0], false)
workspace, agent, err := getWorkspaceAndAgent(cmd, client, codersdk.Me, args[0], false)
if err != nil {
return err
}
+2 -3
View File
@@ -9,7 +9,6 @@ import (
"golang.org/x/xerrors"
"github.com/google/uuid"
"github.com/kirsle/configdir"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
@@ -180,7 +179,7 @@ func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.
// namedWorkspace fetches and returns a workspace by an identifier, which may be either
// a bare name (for a workspace owned by the current user) or a "user/workspace" combination,
// where user is either a username or UUID.
func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, orgID uuid.UUID, identifier string) (codersdk.Workspace, error) {
func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, identifier string) (codersdk.Workspace, error) {
parts := strings.Split(identifier, "/")
var owner, name string
@@ -195,7 +194,7 @@ func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, orgID uuid.UUID
return codersdk.Workspace{}, xerrors.Errorf("invalid workspace name: %q", identifier)
}
return client.WorkspaceByOwnerAndName(cmd.Context(), orgID, owner, name)
return client.WorkspaceByOwnerAndName(cmd.Context(), owner, name)
}
// createConfig consumes the global configuration flag to produce a config root.
+3 -5
View File
@@ -412,11 +412,9 @@ func server() *cobra.Command {
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit"))
if dev {
organizations, err := client.OrganizationsByUser(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("get organizations: %w", err)
}
workspaces, err := client.WorkspacesByOwner(cmd.Context(), organizations[0].ID, codersdk.Me)
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return xerrors.Errorf("get workspaces: %w", err)
}
+1 -5
View File
@@ -18,11 +18,7 @@ func show() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
+6 -8
View File
@@ -48,10 +48,6 @@ func ssh() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
if shuffle {
err := cobra.ExactArgs(0)(cmd, args)
@@ -65,7 +61,7 @@ func ssh() *cobra.Command {
}
}
workspace, agent, err := getWorkspaceAndAgent(cmd, client, organization.ID, codersdk.Me, args[0], shuffle)
workspace, agent, err := getWorkspaceAndAgent(cmd, client, codersdk.Me, args[0], shuffle)
if err != nil {
return err
}
@@ -185,7 +181,7 @@ func ssh() *cobra.Command {
// getWorkspaceAgent returns the workspace and agent selected using either the
// `<workspace>[.<agent>]` syntax via `in` or picks a random workspace and agent
// if `shuffle` is true.
func getWorkspaceAndAgent(cmd *cobra.Command, client *codersdk.Client, orgID uuid.UUID, userID string, in string, shuffle bool) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
func getWorkspaceAndAgent(cmd *cobra.Command, client *codersdk.Client, userID string, in string, shuffle bool) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
ctx := cmd.Context()
var (
@@ -194,7 +190,9 @@ func getWorkspaceAndAgent(cmd *cobra.Command, client *codersdk.Client, orgID uui
err error
)
if shuffle {
workspaces, err := client.WorkspacesByOwner(cmd.Context(), orgID, userID)
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
@@ -207,7 +205,7 @@ func getWorkspaceAndAgent(cmd *cobra.Command, client *codersdk.Client, orgID uui
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
} else {
workspace, err = namedWorkspace(cmd, client, orgID, workspaceParts[0])
workspace, err = namedWorkspace(cmd, client, workspaceParts[0])
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
+1 -5
View File
@@ -28,11 +28,7 @@ func start() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
+2 -12
View File
@@ -30,12 +30,7 @@ func statePull() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
@@ -76,12 +71,7 @@ func statePush() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
+1 -5
View File
@@ -28,11 +28,7 @@ func stop() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
+3 -15
View File
@@ -39,12 +39,8 @@ func ttlShow() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current org: %w", err)
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
@@ -72,12 +68,8 @@ func ttlset() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current org: %w", err)
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
@@ -120,12 +112,8 @@ func ttlunset() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current org: %w", err)
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
+1 -5
View File
@@ -19,11 +19,7 @@ func update() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, organization.ID, args[0])
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
+2 -9
View File
@@ -152,15 +152,7 @@ func New(options *Options) *API {
r.Get("/", api.templatesByOrganization)
r.Get("/{templatename}", api.templateByOrganizationAndName)
})
r.Route("/workspaces", func(r chi.Router) {
r.Post("/", api.postWorkspacesByOrganization)
r.Get("/", api.workspacesByOrganization)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
r.Get("/", api.workspacesByOwner)
})
})
r.Post("/workspaces", api.postWorkspacesByOrganization)
r.Route("/members", func(r chi.Router) {
r.Get("/roles", api.assignableOrgRoles)
r.Route("/{user}", func(r chi.Router) {
@@ -259,6 +251,7 @@ func New(options *Options) *API {
r.Get("/", api.organizationsByUser)
r.Get("/{organizationname}", api.organizationByUserAndName)
})
r.Get("/workspace/{workspacename}", api.workspaceByOwnerAndName)
r.Get("/gitsshkey", api.gitSSHKey)
r.Put("/gitsshkey", api.regenerateGitSSHKey)
})
+5 -11
View File
@@ -147,22 +147,16 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
"GET:/api/v2/users/{user}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
"GET:/api/v2/organizations/{organization}/workspaces/{user}": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspace}": {
AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()),
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
"GET:/api/v2/users/{user}/workspace/{workspacename}": {
AssertObject: rbac.ResourceWorkspace,
AssertAction: rbac.ActionRead,
},
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
AssertAction: rbac.ActionRead,
AssertObject: workspaceRBACObj,
},
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspacename}": {
AssertAction: rbac.ActionRead,
AssertObject: workspaceRBACObj,
},
"GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
AssertAction: rbac.ActionRead,
AssertObject: workspaceRBACObj,
-67
View File
@@ -100,35 +100,6 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, owner))
}
func (api *API) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
OrganizationID: organization.ID,
Deleted: false,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
// Rbac filter
workspaces = AuthorizeFilter(api, r, rbac.ActionRead, workspaces)
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("convert workspaces: %s", err),
})
return
}
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
}
// workspaces returns all workspaces a user can read.
// Optional filters with query params
func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
@@ -189,38 +160,8 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
}
func (api *API) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
owner := httpmw.UserParam(r)
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
OwnerID: owner.ID,
Deleted: false,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
// Only return workspaces the user can read
workspaces = AuthorizeFilter(api, r, rbac.ActionRead, workspaces)
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("convert workspaces: %s", err),
})
return
}
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
}
func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
owner := httpmw.UserParam(r)
organization := httpmw.OrganizationParam(r)
workspaceName := chi.URLParam(r, "workspacename")
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
@@ -238,14 +179,6 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
})
return
}
if workspace.OrganizationID != organization.ID {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("workspace is not owned by organization %q", organization.Name),
})
return
}
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
return
}
+6 -96
View File
@@ -210,107 +210,15 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
})
}
func TestWorkspacesByOrganization(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID)
require.NoError(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
workspaces, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID)
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
}
func TestWorkspacesByOwner(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
require.NoError(t, err)
})
t.Run("ListMine", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
me, err := client.User(context.Background(), codersdk.Me)
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
// Create noise workspace that should be filtered out
other := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_ = coderdtest.CreateWorkspace(t, other, user.OrganizationID, template.ID)
// Use a username
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
OrganizationID: user.OrganizationID,
Owner: me.Username,
})
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
t.Run("ListName", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
w := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
// Create noise workspace that should be filtered out
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
// Use name filter
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
Name: w.Name,
})
require.NoError(t, err)
require.Len(t, workspaces, 1)
// Create same name workspace that should be included
other := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_ = coderdtest.CreateWorkspace(t, other, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { cwr.Name = w.Name })
workspaces, err = client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
Name: w.Name,
})
require.NoError(t, err)
require.Len(t, workspaces, 2)
})
}
func TestWorkspaceByOwnerAndName(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, "something")
_, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "something")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
@@ -320,7 +228,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, workspace.Name)
_, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, workspace.Name)
require.NoError(t, err)
})
}
@@ -440,7 +348,9 @@ func TestPostWorkspaceBuild(t *testing.T) {
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, user.UserID.String())
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
Owner: user.UserID.String(),
})
require.NoError(t, err)
require.Len(t, workspaces, 0)
})
-48
View File
@@ -200,51 +200,3 @@ func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID,
var workspace Workspace
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}
// WorkspacesByOrganization returns all workspaces in the specified organization.
func (c *Client) WorkspacesByOrganization(ctx context.Context, organizationID uuid.UUID) ([]Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}
// WorkspacesByOwner returns all workspaces contained in the organization owned by the user.
func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID uuid.UUID, user string) ([]Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, user), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}
// WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name.
func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, organization uuid.UUID, owner string, name string) (Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, owner, name), nil)
if err != nil {
return Workspace{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Workspace{}, readBodyAsError(res)
}
var workspace Workspace
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}
+16
View File
@@ -238,3 +238,19 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Work
var workspaces []Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}
// WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name.
func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name string) (Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspace/%s", owner, name), nil)
if err != nil {
return Workspace{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Workspace{}, readBodyAsError(res)
}
var workspace Workspace
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}
+1 -4
View File
@@ -139,13 +139,10 @@ export const getWorkspaces = async (filter?: TypesGen.WorkspaceFilter): Promise<
}
export const getWorkspaceByOwnerAndName = async (
organizationID: string,
username = "me",
workspaceName: string,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(
`/api/v2/organizations/${organizationID}/workspaces/${username}/${workspaceName}`,
)
const response = await axios.get<TypesGen.Workspace>(`/api/v2/users/${username}/workspace/${workspaceName}`)
return response.data
}
@@ -54,25 +54,10 @@ describe("TerminalPage", () => {
history.push(`/some-user/${MockWorkspace.name}/terminal`)
})
it("shows an error if fetching organizations fails", async () => {
// Given
server.use(
rest.get("/api/v2/users/me/organizations", async (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: "nope" }))
}),
)
// When
const { container } = renderTerminal()
// Then
await expectTerminalText(container, Language.organizationsErrorMessagePrefix)
})
it("shows an error if fetching workspace fails", async () => {
// Given
server.use(
rest.get("/api/v2/organizations/:organizationId/workspaces/:userName/:workspaceName", (req, res, ctx) => {
rest.get("/api/v2/users/:userId/workspace/:workspaceName", (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ id: "workspace-id" }))
}),
)
+2 -17
View File
@@ -11,7 +11,6 @@ import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
import { terminalMachine } from "../../xServices/terminal/terminalXService"
export const Language = {
organizationsErrorMessagePrefix: "Unable to fetch organizations: ",
workspaceErrorMessagePrefix: "Unable to fetch workspace: ",
workspaceAgentErrorMessagePrefix: "Unable to fetch workspace agent: ",
websocketErrorMessagePrefix: "WebSocket failed: ",
@@ -58,8 +57,7 @@ const TerminalPage: FC<{
})
const isConnected = terminalState.matches("connected")
const isDisconnected = terminalState.matches("disconnected")
const { organizationsError, workspaceError, workspaceAgentError, workspaceAgent, websocketError } =
terminalState.context
const { workspaceError, workspaceAgentError, workspaceAgent, websocketError } = terminalState.context
// Create the terminal!
useEffect(() => {
@@ -145,9 +143,6 @@ const TerminalPage: FC<{
terminal.options = {
disableStdin: true,
}
if (organizationsError instanceof Error) {
terminal.writeln(Language.organizationsErrorMessagePrefix + organizationsError.message)
}
if (workspaceError instanceof Error) {
terminal.writeln(Language.workspaceErrorMessagePrefix + workspaceError.message)
}
@@ -180,17 +175,7 @@ const TerminalPage: FC<{
width: terminal.cols,
},
})
}, [
workspaceError,
organizationsError,
workspaceAgentError,
websocketError,
workspaceAgent,
terminal,
fitAddon,
isConnected,
sendEvent,
])
}, [workspaceError, workspaceAgentError, websocketError, workspaceAgent, terminal, fitAddon, isConnected, sendEvent])
return (
<>
+3 -15
View File
@@ -42,9 +42,6 @@ export const handlers = [
rest.post("/api/v2/users", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockUser))
}),
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
rest.get("/api/v2/users/me/organizations", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([M.MockOrganization]))
}),
@@ -83,23 +80,14 @@ export const handlers = [
rest.get("/api/v2/users/:userId/gitsshkey", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockGitSSHKey))
}),
rest.get("/api/v2/users/:userId/workspace/:workspaceName", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
// workspaces
rest.get("/api/v2/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json([M.MockWorkspace]))
}),
rest.get("/api/v2/organizations/:organizationId/workspaces/:userName/:workspaceName", (req, res, ctx) => {
if (req.params.workspaceName !== M.MockWorkspace.name) {
return res(
ctx.status(404),
ctx.json({
message: "workspace not found",
}),
)
} else {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}
}),
rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
@@ -4,8 +4,6 @@ import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
export interface TerminalContext {
organizationsError?: Error | unknown
organizations?: TypesGen.Organization[]
workspaceError?: Error | unknown
workspace?: TypesGen.Workspace
workspaceAgent?: TypesGen.WorkspaceAgent
@@ -37,9 +35,6 @@ export const terminalMachine =
context: {} as TerminalContext,
events: {} as TerminalEvent,
services: {} as {
getOrganizations: {
data: TypesGen.Organization[]
}
getWorkspace: {
data: TypesGen.Workspace
}
@@ -52,27 +47,8 @@ export const terminalMachine =
},
},
id: "terminalState",
initial: "gettingOrganizations",
initial: "gettingWorkspace",
states: {
gettingOrganizations: {
invoke: {
src: "getOrganizations",
id: "getOrganizations",
onDone: [
{
actions: ["assignOrganizations", "clearOrganizationsError"],
target: "gettingWorkspace",
},
],
onError: [
{
actions: "assignOrganizationsError",
target: "disconnected",
},
],
},
tags: "loading",
},
gettingWorkspace: {
invoke: {
src: "getWorkspace",
@@ -145,7 +121,7 @@ export const terminalMachine =
on: {
CONNECT: {
actions: "assignConnection",
target: "gettingOrganizations",
target: "gettingWorkspace",
},
},
},
@@ -153,12 +129,11 @@ export const terminalMachine =
},
{
services: {
getOrganizations: API.getOrganizations,
getWorkspace: async (context) => {
if (!context.organizations || !context.workspaceName) {
throw new Error("organizations or workspace name not set")
if (!context.workspaceName) {
throw new Error("workspace name not set")
}
return API.getWorkspaceByOwnerAndName(context.organizations[0].id, context.username, context.workspaceName)
return API.getWorkspaceByOwnerAndName(context.username, context.workspaceName)
},
getWorkspaceAgent: async (context) => {
if (!context.workspace || !context.workspaceName) {
@@ -220,16 +195,6 @@ export const terminalMachine =
reconnection: event.reconnection ?? context.reconnection,
workspaceName: event.workspaceName ?? context.workspaceName,
})),
assignOrganizations: assign({
organizations: (_, event) => event.data,
}),
assignOrganizationsError: assign({
organizationsError: (_, event) => event.data,
}),
clearOrganizationsError: assign((context) => ({
...context,
organizationsError: undefined,
})),
assignWorkspace: assign({
workspace: (_, event) => event.data,
}),