Skip to content

Commit 266af06

Browse files
committed
add API endpoints and test (currently fails)
1 parent 804f4a1 commit 266af06

File tree

8 files changed

+359
-0
lines changed

8 files changed

+359
-0
lines changed

coderd/apidoc/docs.go

+62
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+54
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/audit/diff.go

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Auditable interface {
1313
database.TemplateVersion |
1414
database.User |
1515
database.Workspace |
16+
database.UserPinnedWorkspace |
1617
database.GitSSHKey |
1718
database.WorkspaceBuild |
1819
database.AuditableGroup |

coderd/coderd.go

+2
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,8 @@ func New(options *Options) *API {
950950
r.Get("/watch", api.watchWorkspace)
951951
r.Put("/extend", api.putExtendWorkspace)
952952
r.Put("/dormant", api.putWorkspaceDormant)
953+
r.Put("/pin", api.putWorkspacePin)
954+
r.Delete("/pin", api.deleteWorkspacePin)
953955
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
954956
r.Get("/resolve-autostart", api.resolveAutostart)
955957
})

coderd/workspaces.go

+88
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,93 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
10211021
httpapi.Write(ctx, rw, code, resp)
10221022
}
10231023

1024+
// @Summary Pin workspace by ID.
1025+
// @ID pin-workspace-by-id
1026+
// @Security CoderSessionToken
1027+
// @Accept json
1028+
// @Tags Workspaces
1029+
// @Param workspace path string true "Workspace ID" format(uuid)
1030+
// @Success 204
1031+
// @Router /workspaces/{workspace}/pin [put]
1032+
func (api *API) putWorkspacePin(rw http.ResponseWriter, r *http.Request) {
1033+
var (
1034+
ctx = r.Context()
1035+
apiKey = httpmw.APIKey(r)
1036+
workspace = httpmw.WorkspaceParam(r)
1037+
auditor = api.Auditor.Load()
1038+
aReq, commitAudit = audit.InitRequest[database.UserPinnedWorkspace](rw, &audit.RequestParams{
1039+
Audit: *auditor,
1040+
Log: api.Logger,
1041+
Request: r,
1042+
Action: database.AuditActionCreate,
1043+
})
1044+
)
1045+
defer commitAudit()
1046+
aReq.Old = database.UserPinnedWorkspace{}
1047+
1048+
err := api.Database.PinWorkspace(ctx, database.PinWorkspaceParams{
1049+
UserID: apiKey.UserID,
1050+
WorkspaceID: workspace.ID,
1051+
})
1052+
if err != nil {
1053+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1054+
Message: "Internal error pinning workspace",
1055+
Detail: err.Error(),
1056+
})
1057+
return
1058+
}
1059+
1060+
aReq.New = database.UserPinnedWorkspace{
1061+
UserID: apiKey.UserID,
1062+
WorkspaceID: workspace.ID,
1063+
}
1064+
1065+
rw.WriteHeader(http.StatusNoContent)
1066+
}
1067+
1068+
// @Summary Unpin workspace by ID.
1069+
// @ID unpin-workspace-by-id
1070+
// @Security CoderSessionToken
1071+
// @Accept json
1072+
// @Tags Workspaces
1073+
// @Param workspace path string true "Workspace ID" format(uuid)
1074+
// @Success 204
1075+
// @Router /workspaces/{workspace}/pin [delete]
1076+
func (api *API) deleteWorkspacePin(rw http.ResponseWriter, r *http.Request) {
1077+
var (
1078+
ctx = r.Context()
1079+
apiKey = httpmw.APIKey(r)
1080+
workspace = httpmw.WorkspaceParam(r)
1081+
auditor = api.Auditor.Load()
1082+
aReq, commitAudit = audit.InitRequest[database.UserPinnedWorkspace](rw, &audit.RequestParams{
1083+
Audit: *auditor,
1084+
Log: api.Logger,
1085+
Request: r,
1086+
Action: database.AuditActionCreate,
1087+
})
1088+
)
1089+
defer commitAudit()
1090+
aReq.Old = database.UserPinnedWorkspace{
1091+
UserID: apiKey.UserID,
1092+
WorkspaceID: workspace.ID,
1093+
}
1094+
1095+
err := api.Database.UnpinWorkspace(ctx, database.UnpinWorkspaceParams{
1096+
UserID: apiKey.UserID,
1097+
WorkspaceID: workspace.ID,
1098+
})
1099+
if err != nil {
1100+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1101+
Message: "Internal error unpinning workspace",
1102+
Detail: err.Error(),
1103+
})
1104+
return
1105+
}
1106+
aReq.New = database.UserPinnedWorkspace{}
1107+
1108+
rw.WriteHeader(http.StatusNoContent)
1109+
}
1110+
10241111
// @Summary Update workspace automatic updates by ID
10251112
// @ID update-workspace-automatic-updates-by-id
10261113
// @Security CoderSessionToken
@@ -1472,6 +1559,7 @@ func convertWorkspace(
14721559
},
14731560
AutomaticUpdates: codersdk.AutomaticUpdates(workspace.AutomaticUpdates),
14741561
AllowRenames: allowRenames,
1562+
// Pinned: pinned, // TODO
14751563
}
14761564
}
14771565

coderd/workspaces_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -2927,4 +2927,79 @@ func TestWorkspaceDormant(t *testing.T) {
29272927
require.NoError(t, err)
29282928
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart)
29292929
})
2930+
2931+
t.Run("PinUnpin", func(t *testing.T) {
2932+
t.Parallel()
2933+
// Given:
2934+
var (
2935+
auditRecorder = audit.NewMock()
2936+
client = coderdtest.New(t, &coderdtest.Options{
2937+
IncludeProvisionerDaemon: true,
2938+
Auditor: auditRecorder,
2939+
})
2940+
owner = coderdtest.CreateFirstUser(t, client)
2941+
version = coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
2942+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
2943+
template = coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
2944+
memberClient, _ = coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
2945+
workspace = coderdtest.CreateWorkspace(t, memberClient, owner.OrganizationID, template.ID)
2946+
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
2947+
)
2948+
2949+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
2950+
defer cancel()
2951+
2952+
// Initially, workspace should not be pinned.
2953+
workspaces, err := memberClient.Workspaces(ctx, codersdk.WorkspaceFilter{})
2954+
require.NoError(t, err)
2955+
require.Len(t, workspaces.Workspaces, 1)
2956+
require.False(t, workspaces.Workspaces[0].Pinned)
2957+
ws, err := memberClient.Workspace(ctx, workspace.ID)
2958+
require.NoError(t, err)
2959+
require.False(t, ws.Pinned)
2960+
2961+
// When member pins workspace
2962+
err = memberClient.PinWorkspace(ctx, workspace.ID)
2963+
require.NoError(t, err)
2964+
2965+
// Then it should be pinned for them
2966+
workspaces, err = memberClient.Workspaces(ctx, codersdk.WorkspaceFilter{})
2967+
require.NoError(t, err)
2968+
require.Len(t, workspaces.Workspaces, 1)
2969+
require.True(t, workspaces.Workspaces[0].Pinned)
2970+
ws, err = memberClient.Workspace(ctx, workspace.ID)
2971+
require.NoError(t, err)
2972+
require.True(t, ws.Pinned)
2973+
2974+
// But not for someone else
2975+
workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
2976+
require.NoError(t, err)
2977+
require.Len(t, workspaces.Workspaces, 1)
2978+
require.False(t, workspaces.Workspaces[0].Pinned)
2979+
ws, err = client.Workspace(ctx, workspace.ID)
2980+
require.NoError(t, err)
2981+
require.False(t, ws.Pinned)
2982+
2983+
// When member unpins workspace
2984+
err = memberClient.UnpinWorkspace(ctx, workspace.ID)
2985+
require.NoError(t, err)
2986+
2987+
// Then it should no longer be pinned for them
2988+
workspaces, err = memberClient.Workspaces(ctx, codersdk.WorkspaceFilter{})
2989+
require.NoError(t, err)
2990+
require.Len(t, workspaces.Workspaces, 1)
2991+
require.False(t, workspaces.Workspaces[0].Pinned)
2992+
ws, err = memberClient.Workspace(ctx, workspace.ID)
2993+
require.NoError(t, err)
2994+
require.False(t, ws.Pinned)
2995+
2996+
// Assert invariant: workspace should remain unpinned for a different user
2997+
workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
2998+
require.NoError(t, err)
2999+
require.Len(t, workspaces.Workspaces, 1)
3000+
require.False(t, workspaces.Workspaces[0].Pinned)
3001+
ws, err = client.Workspace(ctx, workspace.ID)
3002+
require.NoError(t, err)
3003+
require.False(t, ws.Pinned)
3004+
})
29303005
}

codersdk/workspaces.go

+25
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type Workspace struct {
5858
Health WorkspaceHealth `json:"health"`
5959
AutomaticUpdates AutomaticUpdates `json:"automatic_updates" enums:"always,never"`
6060
AllowRenames bool `json:"allow_renames"`
61+
Pinned bool `json:"pinned"`
6162
}
6263

6364
func (w Workspace) FullName() string {
@@ -471,6 +472,30 @@ func (c *Client) ResolveAutostart(ctx context.Context, workspaceID string) (Reso
471472
return response, json.NewDecoder(res.Body).Decode(&response)
472473
}
473474

475+
func (c *Client) PinWorkspace(ctx context.Context, workspaceID uuid.UUID) error {
476+
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/workspaces/%s/pin", workspaceID), nil)
477+
if err != nil {
478+
return err
479+
}
480+
defer res.Body.Close()
481+
if res.StatusCode != http.StatusNoContent {
482+
return err
483+
}
484+
return nil
485+
}
486+
487+
func (c *Client) UnpinWorkspace(ctx context.Context, workspaceID uuid.UUID) error {
488+
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/workspaces/%s/pin", workspaceID), nil)
489+
if err != nil {
490+
return err
491+
}
492+
defer res.Body.Close()
493+
if res.StatusCode != http.StatusNoContent {
494+
return err
495+
}
496+
return nil
497+
}
498+
474499
// WorkspaceNotifyChannel is the PostgreSQL NOTIFY
475500
// channel to listen for updates on. The payload is empty,
476501
// because the size of a workspace payload can be very large.

0 commit comments

Comments
 (0)