Skip to content

Commit 0a83477

Browse files
committed
Add endpoints for extracting singular history
1 parent d062b5b commit 0a83477

13 files changed

+240
-88
lines changed

coderd/coderd.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ func New(options *Options) http.Handler {
6464
r.Route("/history", func(r chi.Router) {
6565
r.Get("/", api.projectHistoryByOrganization)
6666
r.Post("/", api.postProjectHistoryByOrganization)
67+
r.Route("/{projecthistory}", func(r chi.Router) {
68+
r.Use(httpmw.ExtractProjectHistoryParam(api.Database))
69+
r.Get("/", api.projectHistoryByOrganizationAndName)
70+
})
6771
})
6872
})
6973
})
@@ -84,7 +88,10 @@ func New(options *Options) http.Handler {
8488
r.Route("/history", func(r chi.Router) {
8589
r.Post("/", api.postWorkspaceHistoryByUser)
8690
r.Get("/", api.workspaceHistoryByUser)
87-
r.Get("/latest", api.latestWorkspaceHistoryByUser)
91+
r.Route("/{workspacehistory}", func(r chi.Router) {
92+
r.Use(httpmw.ExtractWorkspaceHistoryParam(options.Database))
93+
r.Get("/", api.workspaceHistoryByName)
94+
})
8895
})
8996
})
9097
})

coderd/projecthistory.go

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"archive/tar"
55
"bytes"
66
"database/sql"
7+
"encoding/json"
78
"errors"
89
"fmt"
910
"net/http"
@@ -12,6 +13,7 @@ import (
1213
"github.com/go-chi/render"
1314
"github.com/google/uuid"
1415
"github.com/moby/moby/pkg/namesgenerator"
16+
"golang.org/x/xerrors"
1517

1618
"github.com/coder/coder/database"
1719
"github.com/coder/coder/httpapi"
@@ -26,6 +28,7 @@ type ProjectHistory struct {
2628
UpdatedAt time.Time `json:"updated_at"`
2729
Name string `json:"name"`
2830
StorageMethod database.ProjectStorageMethod `json:"storage_method"`
31+
Import ProvisionerJob `json:"import"`
2932
}
3033

3134
// CreateProjectHistoryRequest enables callers to create a new Project Version.
@@ -50,12 +53,33 @@ func (api *api) projectHistoryByOrganization(rw http.ResponseWriter, r *http.Req
5053
}
5154
apiHistory := make([]ProjectHistory, 0)
5255
for _, version := range history {
53-
apiHistory = append(apiHistory, convertProjectHistory(version))
56+
job, err := api.Database.GetProvisionerJobByID(r.Context(), version.ImportJobID)
57+
if err != nil {
58+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
59+
Message: fmt.Sprintf("get provisioner job: %s", err),
60+
})
61+
return
62+
}
63+
apiHistory = append(apiHistory, convertProjectHistory(version, job))
5464
}
5565
render.Status(r, http.StatusOK)
5666
render.JSON(rw, r, apiHistory)
5767
}
5868

69+
// Return a single project history by organization and name.
70+
func (api *api) projectHistoryByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
71+
projectHistory := httpmw.ProjectHistoryParam(r)
72+
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectHistory.ImportJobID)
73+
if err != nil {
74+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
75+
Message: fmt.Sprintf("get provisioner job: %s", err),
76+
})
77+
return
78+
}
79+
render.Status(r, http.StatusOK)
80+
render.JSON(rw, r, convertProjectHistory(projectHistory, job))
81+
}
82+
5983
// Creates a new version of the project. An import job is queued to parse
6084
// the storage method provided. Once completed, the import job will specify
6185
// the version as latest.
@@ -82,38 +106,68 @@ func (api *api) postProjectHistoryByOrganization(rw http.ResponseWriter, r *http
82106
return
83107
}
84108

109+
apiKey := httpmw.APIKey(r)
85110
project := httpmw.ProjectParam(r)
86-
history, err := api.Database.InsertProjectHistory(r.Context(), database.InsertProjectHistoryParams{
87-
ID: uuid.New(),
88-
ProjectID: project.ID,
89-
CreatedAt: database.Now(),
90-
UpdatedAt: database.Now(),
91-
Name: namesgenerator.GetRandomName(1),
92-
StorageMethod: createProjectVersion.StorageMethod,
93-
StorageSource: createProjectVersion.StorageSource,
94-
// TODO: Make this do something!
95-
ImportJobID: uuid.New(),
111+
112+
var provisionerJob database.ProvisionerJob
113+
var projectHistory database.ProjectHistory
114+
err := api.Database.InTx(func(db database.Store) error {
115+
projectHistoryID := uuid.New()
116+
input, err := json.Marshal(projectImportJob{
117+
ProjectHistoryID: projectHistoryID,
118+
})
119+
if err != nil {
120+
return xerrors.Errorf("marshal import job: %w", err)
121+
}
122+
123+
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
124+
ID: uuid.New(),
125+
CreatedAt: database.Now(),
126+
UpdatedAt: database.Now(),
127+
InitiatorID: apiKey.UserID,
128+
Provisioner: project.Provisioner,
129+
Type: database.ProvisionerJobTypeProjectImport,
130+
ProjectID: project.ID,
131+
Input: input,
132+
})
133+
if err != nil {
134+
return xerrors.Errorf("insert provisioner job: %w", err)
135+
}
136+
137+
projectHistory, err = api.Database.InsertProjectHistory(r.Context(), database.InsertProjectHistoryParams{
138+
ID: projectHistoryID,
139+
ProjectID: project.ID,
140+
CreatedAt: database.Now(),
141+
UpdatedAt: database.Now(),
142+
Name: namesgenerator.GetRandomName(1),
143+
StorageMethod: createProjectVersion.StorageMethod,
144+
StorageSource: createProjectVersion.StorageSource,
145+
ImportJobID: provisionerJob.ID,
146+
})
147+
if err != nil {
148+
return xerrors.Errorf("insert project history: %s", err)
149+
}
150+
return nil
96151
})
97152
if err != nil {
98153
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
99-
Message: fmt.Sprintf("insert project history: %s", err),
154+
Message: err.Error(),
100155
})
101156
return
102157
}
103158

104-
// TODO: A job to process the new version should occur here.
105-
106159
render.Status(r, http.StatusCreated)
107-
render.JSON(rw, r, convertProjectHistory(history))
160+
render.JSON(rw, r, convertProjectHistory(projectHistory, provisionerJob))
108161
}
109162

110-
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
163+
func convertProjectHistory(history database.ProjectHistory, job database.ProvisionerJob) ProjectHistory {
111164
return ProjectHistory{
112165
ID: history.ID,
113166
ProjectID: history.ProjectID,
114167
CreatedAt: history.CreatedAt,
115168
UpdatedAt: history.UpdatedAt,
116169
Name: history.Name,
170+
Import: convertProvisionerJob(job),
117171
}
118172
}
119173

coderd/projecthistory_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestProjectHistory(t *testing.T) {
2525
Provisioner: database.ProvisionerTypeTerraform,
2626
})
2727
require.NoError(t, err)
28-
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
28+
versions, err := server.Client.ListProjectHistory(context.Background(), user.Organization, project.Name)
2929
require.NoError(t, err)
3030
require.Len(t, versions, 0)
3131
})
@@ -48,14 +48,17 @@ func TestProjectHistory(t *testing.T) {
4848
require.NoError(t, err)
4949
_, err = writer.Write(make([]byte, 1<<10))
5050
require.NoError(t, err)
51-
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
51+
history, err := server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
5252
StorageMethod: database.ProjectStorageMethodInlineArchive,
5353
StorageSource: buffer.Bytes(),
5454
})
5555
require.NoError(t, err)
56-
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
56+
versions, err := server.Client.ListProjectHistory(context.Background(), user.Organization, project.Name)
5757
require.NoError(t, err)
5858
require.Len(t, versions, 1)
59+
60+
_, err = server.Client.ProjectHistory(context.Background(), user.Organization, project.Name, history.Name)
61+
require.NoError(t, err)
5962
})
6063

6164
t.Run("CreateHistoryArchiveTooBig", func(t *testing.T) {

coderd/provisionerdaemons.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
241241
if err != nil {
242242
return nil, failJob(fmt.Sprintf("get project history: %s", err))
243243
}
244+
245+
protoJob.Type = &proto.AcquiredJob_ProjectImport_{
246+
ProjectImport: &proto.AcquiredJob_ProjectImport{
247+
ProjectHistoryId: projectHistory.ID.String(),
248+
ProjectHistoryName: projectHistory.Name,
249+
},
250+
}
244251
}
245252
switch projectHistory.StorageMethod {
246253
case database.ProjectStorageMethodInlineArchive:

coderd/provisioners.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import (
1010

1111
type ProvisionerJobStatus string
1212

13+
// Completed returns whether the job is still processing.
14+
func (p ProvisionerJobStatus) Completed() bool {
15+
return p == ProvisionerJobStatusSucceeded || p == ProvisionerJobStatusFailed
16+
}
17+
1318
const (
1419
ProvisionerJobStatusPending ProvisionerJobStatus = "pending"
1520
ProvisionerJobStatusRunning ProvisionerJobStatus = "running"

coderd/workspacehistory.go

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type WorkspaceHistory struct {
2929
AfterID uuid.UUID `json:"after_id"`
3030
Transition database.WorkspaceTransition `json:"transition"`
3131
Initiator string `json:"initiator"`
32-
Job ProvisionerJob `json:"job"`
32+
Provision ProvisionerJob `json:"provision"`
3333
}
3434

3535
// CreateWorkspaceHistoryRequest provides options to update the latest workspace history.
@@ -62,6 +62,27 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
6262
})
6363
return
6464
}
65+
projectHistoryJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectHistory.ImportJobID)
66+
if err != nil {
67+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
68+
Message: fmt.Sprintf("get provisioner job: %s", err),
69+
})
70+
return
71+
}
72+
projectHistoryJobStatus := convertProvisionerJob(projectHistoryJob).Status
73+
switch projectHistoryJobStatus {
74+
case ProvisionerJobStatusPending, ProvisionerJobStatusRunning:
75+
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
76+
Message: fmt.Sprintf("The provided project history is %s. Wait for it to complete importing!", projectHistoryJobStatus),
77+
})
78+
return
79+
case ProvisionerJobStatusFailed:
80+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
81+
Message: fmt.Sprintf("The provided project history %q has failed to import. You cannot create workspaces using it!", projectHistory.Name),
82+
})
83+
return
84+
}
85+
6586
project, err := api.Database.GetProjectByID(r.Context(), projectHistory.ProjectID)
6687
if err != nil {
6788
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -75,15 +96,11 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
7596
priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
7697
if err == nil {
7798
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID)
78-
if err == nil {
79-
convertedJob := convertProvisionerJob(priorJob)
80-
if convertedJob.Status == ProvisionerJobStatusPending ||
81-
convertedJob.Status == ProvisionerJobStatusRunning {
82-
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
83-
Message: "a workspace build is already active",
84-
})
85-
return
86-
}
99+
if err == nil && convertProvisionerJob(priorJob).Status.Completed() {
100+
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
101+
Message: "a workspace build is already active",
102+
})
103+
return
87104
}
88105

89106
priorHistoryID = uuid.NullUUID{
@@ -200,24 +217,9 @@ func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request)
200217
render.JSON(rw, r, apiHistory)
201218
}
202219

203-
// Returns the latest workspace history. This works by querying for history without "after" set.
204-
func (api *api) latestWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
205-
workspace := httpmw.WorkspaceParam(r)
206-
207-
history, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
208-
if errors.Is(err, sql.ErrNoRows) {
209-
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
210-
Message: "workspace has no history",
211-
})
212-
return
213-
}
214-
if err != nil {
215-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
216-
Message: fmt.Sprintf("get workspace history: %s", err),
217-
})
218-
return
219-
}
220-
job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID)
220+
func (api *api) workspaceHistoryByName(rw http.ResponseWriter, r *http.Request) {
221+
workspaceHistory := httpmw.WorkspaceHistoryParam(r)
222+
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceHistory.ProvisionJobID)
221223
if err != nil {
222224
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
223225
Message: fmt.Sprintf("get provisioner job: %s", err),
@@ -226,7 +228,7 @@ func (api *api) latestWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Req
226228
}
227229

228230
render.Status(r, http.StatusOK)
229-
render.JSON(rw, r, convertWorkspaceHistory(history, job))
231+
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory, job))
230232
}
231233

232234
// Converts the internal history representation to a public external-facing model.
@@ -242,7 +244,7 @@ func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory, provisi
242244
AfterID: workspaceHistory.AfterID.UUID,
243245
Transition: workspaceHistory.Transition,
244246
Initiator: workspaceHistory.Initiator,
245-
Job: convertProvisionerJob(provisionerJob),
247+
Provision: convertProvisionerJob(provisionerJob),
246248
})
247249
}
248250

0 commit comments

Comments
 (0)