Skip to content

Commit 29b76a0

Browse files
committed
Add querying for workspace history
1 parent 5b343c2 commit 29b76a0

File tree

12 files changed

+502
-85
lines changed

12 files changed

+502
-85
lines changed

coderd/coderd.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ func New(options *Options) http.Handler {
6262
r.Route("/{project}", func(r chi.Router) {
6363
r.Use(httpmw.ExtractProjectParam(options.Database))
6464
r.Get("/", projects.project)
65-
r.Route("/versions", func(r chi.Router) {
66-
r.Get("/", projects.projectVersions)
67-
r.Post("/", projects.createProjectVersion)
65+
r.Route("/history", func(r chi.Router) {
66+
r.Get("/", projects.projectHistory)
67+
r.Post("/", projects.createProjectHistory)
6868
})
6969
})
7070
})
@@ -90,6 +90,11 @@ func New(options *Options) http.Handler {
9090
httpmw.ExtractWorkspaceParam(options.Database),
9191
)
9292
r.Get("/", workspaces.workspace)
93+
r.Route("/history", func(r chi.Router) {
94+
r.Post("/", workspaces.createWorkspaceBuild)
95+
r.Get("/", workspaces.allWorkspaceHistory)
96+
r.Get("/latest", workspaces.latestWorkspaceHistory)
97+
})
9398
})
9499
})
95100
r.NotFound(site.Handler().ServeHTTP)

coderd/projects.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424
// abstracted for ease of change later on.
2525
type Project database.Project
2626

27-
// ProjectVersion is the JSON representation of a Coder project version.
28-
type ProjectVersion struct {
27+
// ProjectHistory is the JSON representation of a Coder project version.
28+
type ProjectHistory struct {
2929
ID uuid.UUID `json:"id"`
3030
ProjectID uuid.UUID `json:"project_id"`
3131
CreatedAt time.Time `json:"created_at"`
@@ -42,7 +42,6 @@ type CreateProjectRequest struct {
4242

4343
// CreateProjectVersionRequest enables callers to create a new Project Version.
4444
type CreateProjectVersionRequest struct {
45-
Name string `json:"name,omitempty" validate:"username"`
4645
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
4746
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
4847
}
@@ -150,8 +149,8 @@ func (*projects) project(rw http.ResponseWriter, r *http.Request) {
150149
render.JSON(rw, r, project)
151150
}
152151

153-
// projectVersions lists versions for a single project.
154-
func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
152+
// projectHistory lists versions for a single project.
153+
func (p *projects) projectHistory(rw http.ResponseWriter, r *http.Request) {
155154
project := httpmw.ProjectParam(r)
156155

157156
history, err := p.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
@@ -164,15 +163,15 @@ func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
164163
})
165164
return
166165
}
167-
versions := make([]ProjectVersion, 0)
166+
versions := make([]ProjectHistory, 0)
168167
for _, version := range history {
169168
versions = append(versions, convertProjectHistory(version))
170169
}
171170
render.Status(r, http.StatusOK)
172171
render.JSON(rw, r, versions)
173172
}
174173

175-
func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request) {
174+
func (p *projects) createProjectHistory(rw http.ResponseWriter, r *http.Request) {
176175
var createProjectVersion CreateProjectVersionRequest
177176
if !httpapi.Read(rw, r, &createProjectVersion) {
178177
return
@@ -218,8 +217,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
218217
render.JSON(rw, r, convertProjectHistory(history))
219218
}
220219

221-
func convertProjectHistory(history database.ProjectHistory) ProjectVersion {
222-
return ProjectVersion{
220+
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
221+
return ProjectHistory{
223222
ID: history.ID,
224223
ProjectID: history.ProjectID,
225224
CreatedAt: history.CreatedAt,

coderd/projects_test.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestProjects(t *testing.T) {
104104
Provisioner: database.ProvisionerTypeTerraform,
105105
})
106106
require.NoError(t, err)
107-
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
107+
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
108108
require.NoError(t, err)
109109
require.Len(t, versions, 0)
110110
})
@@ -127,13 +127,12 @@ func TestProjects(t *testing.T) {
127127
require.NoError(t, err)
128128
_, err = writer.Write(make([]byte, 1<<10))
129129
require.NoError(t, err)
130-
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
131-
Name: "moo",
130+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
132131
StorageMethod: database.ProjectStorageMethodInlineArchive,
133132
StorageSource: buffer.Bytes(),
134133
})
135134
require.NoError(t, err)
136-
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
135+
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
137136
require.NoError(t, err)
138137
require.Len(t, versions, 1)
139138
})
@@ -156,8 +155,7 @@ func TestProjects(t *testing.T) {
156155
require.NoError(t, err)
157156
_, err = writer.Write(make([]byte, 1<<21))
158157
require.NoError(t, err)
159-
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
160-
Name: "moo",
158+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
161159
StorageMethod: database.ProjectStorageMethodInlineArchive,
162160
StorageSource: buffer.Bytes(),
163161
})
@@ -173,8 +171,7 @@ func TestProjects(t *testing.T) {
173171
Provisioner: database.ProvisionerTypeTerraform,
174172
})
175173
require.NoError(t, err)
176-
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
177-
Name: "moo",
174+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
178175
StorageMethod: database.ProjectStorageMethodInlineArchive,
179176
StorageSource: []byte{},
180177
})

coderd/workspaces.go

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"time"
89

910
"github.com/go-chi/render"
1011
"github.com/google/uuid"
12+
"golang.org/x/xerrors"
1113

1214
"github.com/coder/coder/database"
1315
"github.com/coder/coder/httpapi"
@@ -19,11 +21,32 @@ import (
1921
// abstract for ease of change later on.
2022
type Workspace database.Workspace
2123

24+
// WorkspaceHistory is the JSON representation of a workspace transitioning
25+
// from state-to-state.
26+
type WorkspaceHistory struct {
27+
ID uuid.UUID `json:"id"`
28+
CreatedAt time.Time `json:"created_at"`
29+
UpdatedAt time.Time `json:"updated_at"`
30+
CompletedAt time.Time `json:"completed_at"`
31+
WorkspaceID uuid.UUID `json:"workspace_id"`
32+
ProjectHistoryID uuid.UUID `json:"project_history_id"`
33+
BeforeID uuid.UUID `json:"before_id"`
34+
AfterID uuid.UUID `json:"after_id"`
35+
Transition database.WorkspaceTransition `json:"transition"`
36+
Initiator string `json:"initiator"`
37+
}
38+
2239
// CreateWorkspaceRequest enables callers to create a new Workspace.
2340
type CreateWorkspaceRequest struct {
2441
Name string `json:"name" validate:"username,required"`
2542
}
2643

44+
// CreateWorkspaceBuildRequest enables callers to create a new workspace build.
45+
type CreateWorkspaceBuildRequest struct {
46+
ProjectHistoryID uuid.UUID `json:"project_history_id" validate:"required"`
47+
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
48+
}
49+
2750
type workspaces struct {
2851
Database database.Store
2952
}
@@ -132,15 +155,169 @@ func (w *workspaces) createWorkspace(rw http.ResponseWriter, r *http.Request) {
132155
render.JSON(rw, r, convertWorkspace(workspace))
133156
}
134157

135-
// workspace returns a new workspace.
136158
func (*workspaces) workspace(rw http.ResponseWriter, r *http.Request) {
137159
workspace := httpmw.WorkspaceParam(r)
138160

139161
render.Status(r, http.StatusOK)
140162
render.JSON(rw, r, convertWorkspace(workspace))
141163
}
142164

165+
func (w *workspaces) allWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
166+
workspace := httpmw.WorkspaceParam(r)
167+
168+
histories, err := w.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID)
169+
if errors.Is(err, sql.ErrNoRows) {
170+
err = nil
171+
}
172+
if err != nil {
173+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
174+
Message: fmt.Sprintf("get workspace history: %s", err),
175+
})
176+
return
177+
}
178+
179+
apiHistory := make([]WorkspaceHistory, 0, len(histories))
180+
for _, history := range histories {
181+
apiHistory = append(apiHistory, convertWorkspaceHistory(history))
182+
}
183+
184+
render.Status(r, http.StatusOK)
185+
render.JSON(rw, r, apiHistory)
186+
}
187+
188+
func (w *workspaces) latestWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
189+
workspace := httpmw.WorkspaceParam(r)
190+
191+
history, err := w.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
192+
if errors.Is(err, sql.ErrNoRows) {
193+
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
194+
Message: "workspace has no history",
195+
})
196+
return
197+
}
198+
if err != nil {
199+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
200+
Message: fmt.Sprintf("get workspace history: %s", err),
201+
})
202+
return
203+
}
204+
205+
render.Status(r, http.StatusOK)
206+
render.JSON(rw, r, convertWorkspaceHistory(history))
207+
}
208+
209+
func (w *workspaces) createWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
210+
var createBuild CreateWorkspaceBuildRequest
211+
if !httpapi.Read(rw, r, &createBuild) {
212+
return
213+
}
214+
user := httpmw.UserParam(r)
215+
workspace := httpmw.WorkspaceParam(r)
216+
projectHistory, err := w.Database.GetProjectHistoryByID(r.Context(), createBuild.ProjectHistoryID)
217+
if errors.Is(err, sql.ErrNoRows) {
218+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
219+
Message: "project history not found",
220+
Errors: []httpapi.Error{{
221+
Field: "project_history_id",
222+
Code: "exists",
223+
}},
224+
})
225+
return
226+
}
227+
if err != nil {
228+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
229+
Message: fmt.Sprintf("get project history: %s", err),
230+
})
231+
return
232+
}
233+
234+
// Store prior history ID if it exists to update it after we create new!
235+
priorHistoryID := uuid.NullUUID{}
236+
priorHistory, err := w.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
237+
if err == nil {
238+
if !priorHistory.CompletedAt.Valid {
239+
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
240+
Message: "a workspace build is already active",
241+
})
242+
return
243+
}
244+
245+
priorHistoryID = uuid.NullUUID{
246+
UUID: priorHistory.ID,
247+
Valid: true,
248+
}
249+
}
250+
if !errors.Is(err, sql.ErrNoRows) {
251+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
252+
Message: fmt.Sprintf("get prior workspace history: %s", err),
253+
})
254+
return
255+
}
256+
257+
var workspaceHistory database.WorkspaceHistory
258+
err = w.Database.InTx(func(db database.Store) error {
259+
workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{
260+
ID: uuid.New(),
261+
CreatedAt: database.Now(),
262+
UpdatedAt: database.Now(),
263+
WorkspaceID: workspace.ID,
264+
ProjectHistoryID: projectHistory.ID,
265+
BeforeID: priorHistoryID,
266+
Initiator: user.ID,
267+
Transition: createBuild.Transition,
268+
// This should create a provision job once that gets implemented!
269+
ProvisionJobID: uuid.New(),
270+
})
271+
if err != nil {
272+
return xerrors.Errorf("insert workspace history: %w", err)
273+
}
274+
275+
if priorHistoryID.Valid {
276+
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
277+
ID: priorHistory.ID,
278+
UpdatedAt: database.Now(),
279+
ProvisionerState: priorHistory.ProvisionerState,
280+
CompletedAt: priorHistory.CompletedAt,
281+
AfterID: uuid.NullUUID{
282+
UUID: workspaceHistory.ID,
283+
Valid: true,
284+
},
285+
})
286+
if err != nil {
287+
return xerrors.Errorf("update prior workspace history: %w", err)
288+
}
289+
}
290+
291+
return nil
292+
})
293+
if err != nil {
294+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
295+
Message: err.Error(),
296+
})
297+
return
298+
}
299+
300+
render.Status(r, http.StatusCreated)
301+
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory))
302+
}
303+
143304
// convertWorkspace consumes the database representation and outputs an API friendly representation.
144305
func convertWorkspace(workspace database.Workspace) Workspace {
145306
return Workspace(workspace)
146307
}
308+
309+
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory {
310+
//nolint:unconvert
311+
return WorkspaceHistory(WorkspaceHistory{
312+
ID: workspaceHistory.ID,
313+
CreatedAt: workspaceHistory.CreatedAt,
314+
UpdatedAt: workspaceHistory.UpdatedAt,
315+
CompletedAt: workspaceHistory.CompletedAt.Time,
316+
WorkspaceID: workspaceHistory.WorkspaceID,
317+
ProjectHistoryID: workspaceHistory.ProjectHistoryID,
318+
BeforeID: workspaceHistory.BeforeID.UUID,
319+
AfterID: workspaceHistory.AfterID.UUID,
320+
Transition: workspaceHistory.Transition,
321+
Initiator: workspaceHistory.Initiator,
322+
})
323+
}

0 commit comments

Comments
 (0)