-
Notifications
You must be signed in to change notification settings - Fork 894
feat: Add provisionerdaemon to coderd #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ea69016
7a9f714
33ecab0
7987596
bc27864
c448e99
d062b5b
0a83477
e87f31d
2a76b60
03ed951
9cc4756
8282393
aa760cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package coderd | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
|
||
"github.com/coder/coder/database" | ||
) | ||
|
||
type ProvisionerJobStatus string | ||
|
||
const ( | ||
ProvisionerJobStatusPending ProvisionerJobStatus = "pending" | ||
ProvisionerJobStatusRunning ProvisionerJobStatus = "running" | ||
ProvisionerJobStatusSucceeded ProvisionerJobStatus = "succeeded" | ||
ProvisionerJobStatusFailed ProvisionerJobStatus = "failed" | ||
) | ||
|
||
type ProvisionerJob struct { | ||
CreatedAt time.Time `json:"created_at"` | ||
UpdatedAt time.Time `json:"updated_at"` | ||
StartedAt *time.Time `json:"started_at,omitempty"` | ||
CancelledAt *time.Time `json:"canceled_at,omitempty"` | ||
CompletedAt *time.Time `json:"completed_at,omitempty"` | ||
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious why both |
||
Status ProvisionerJobStatus `json:"status"` | ||
Error string `json:"error,omitempty"` | ||
Provisioner database.ProvisionerType `json:"provisioner"` | ||
WorkerID *uuid.UUID `json:"worker_id,omitempty"` | ||
} | ||
|
||
func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJob { | ||
job := ProvisionerJob{ | ||
CreatedAt: provisionerJob.CreatedAt, | ||
UpdatedAt: provisionerJob.UpdatedAt, | ||
Error: provisionerJob.Error.String, | ||
Provisioner: provisionerJob.Provisioner, | ||
} | ||
if provisionerJob.StartedAt.Valid { | ||
job.StartedAt = &provisionerJob.StartedAt.Time | ||
} | ||
if provisionerJob.CancelledAt.Valid { | ||
job.CancelledAt = &provisionerJob.CancelledAt.Time | ||
} | ||
if provisionerJob.CompletedAt.Valid { | ||
job.CompletedAt = &provisionerJob.CompletedAt.Time | ||
} | ||
if provisionerJob.WorkerID.Valid { | ||
job.WorkerID = &provisionerJob.WorkerID.UUID | ||
} | ||
|
||
switch { | ||
case provisionerJob.CancelledAt.Valid: | ||
job.Status = ProvisionerJobStatusFailed | ||
case !provisionerJob.StartedAt.Valid: | ||
job.Status = ProvisionerJobStatusPending | ||
case provisionerJob.CompletedAt.Valid: | ||
job.Status = ProvisionerJobStatusSucceeded | ||
case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second: | ||
job.Status = ProvisionerJobStatusFailed | ||
job.Error = "Worker failed to update job in time." | ||
default: | ||
job.Status = ProvisionerJobStatusRunning | ||
} | ||
|
||
return job | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package coderd | |
|
||
import ( | ||
"database/sql" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
|
@@ -22,13 +23,13 @@ type WorkspaceHistory struct { | |
ID uuid.UUID `json:"id"` | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
CreatedAt time.Time `json:"created_at"` | ||
UpdatedAt time.Time `json:"updated_at"` | ||
CompletedAt time.Time `json:"completed_at"` | ||
WorkspaceID uuid.UUID `json:"workspace_id"` | ||
ProjectHistoryID uuid.UUID `json:"project_history_id"` | ||
BeforeID uuid.UUID `json:"before_id"` | ||
AfterID uuid.UUID `json:"after_id"` | ||
Transition database.WorkspaceTransition `json:"transition"` | ||
Initiator string `json:"initiator"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not an issue with this PR, but I worry that this schema will make it difficult for a user to change names. That username-change will have to percolate through all these places we're storing their name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a user ID. It's not very clear, so I should add a comment. We can't use the |
||
Job ProvisionerJob `json:"job"` | ||
} | ||
|
||
// CreateWorkspaceHistoryRequest provides options to update the latest workspace history. | ||
|
@@ -37,8 +38,6 @@ type CreateWorkspaceHistoryRequest struct { | |
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` | ||
} | ||
|
||
// Begins transitioning a workspace to new state. This queues a provision job to asynchronously | ||
// update the underlying infrastructure. Only one historical transition can occur at a time. | ||
func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) { | ||
var createBuild CreateWorkspaceHistoryRequest | ||
if !httpapi.Read(rw, r, &createBuild) { | ||
|
@@ -63,16 +62,28 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque | |
}) | ||
return | ||
} | ||
project, err := api.Database.GetProjectByID(r.Context(), projectHistory.ProjectID) | ||
if err != nil { | ||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ | ||
Message: fmt.Sprintf("get project: %s", err), | ||
}) | ||
return | ||
} | ||
|
||
// Store prior history ID if it exists to update it after we create new! | ||
priorHistoryID := uuid.NullUUID{} | ||
priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) | ||
if err == nil { | ||
if !priorHistory.CompletedAt.Valid { | ||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{ | ||
Message: "a workspace build is already active", | ||
}) | ||
return | ||
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID) | ||
if err == nil { | ||
convertedJob := convertProvisionerJob(priorJob) | ||
if convertedJob.Status == ProvisionerJobStatusPending || | ||
convertedJob.Status == ProvisionerJobStatusRunning { | ||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{ | ||
Message: "a workspace build is already active", | ||
}) | ||
return | ||
} | ||
} | ||
|
||
priorHistoryID = uuid.NullUUID{ | ||
|
@@ -87,10 +98,34 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque | |
return | ||
} | ||
|
||
var provisionerJob database.ProvisionerJob | ||
var workspaceHistory database.WorkspaceHistory | ||
// This must happen in a transaction to ensure history can be inserted, and | ||
// the prior history can update it's "after" column to point at the new. | ||
err = api.Database.InTx(func(db database.Store) error { | ||
// Generate the ID before-hand so the provisioner job is aware of it! | ||
workspaceHistoryID := uuid.New() | ||
input, err := json.Marshal(workspaceProvisionJob{ | ||
WorkspaceHistoryID: workspaceHistoryID, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("marshal provision job: %w", err) | ||
} | ||
|
||
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ | ||
ID: uuid.New(), | ||
CreatedAt: database.Now(), | ||
UpdatedAt: database.Now(), | ||
InitiatorID: user.ID, | ||
Provisioner: project.Provisioner, | ||
Type: database.ProvisionerJobTypeWorkspaceProvision, | ||
ProjectID: project.ID, | ||
Input: input, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("insert provisioner job: %w", err) | ||
} | ||
|
||
workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{ | ||
ID: uuid.New(), | ||
CreatedAt: database.Now(), | ||
|
@@ -100,8 +135,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque | |
BeforeID: priorHistoryID, | ||
Initiator: user.ID, | ||
Transition: createBuild.Transition, | ||
// This should create a provision job once that gets implemented! | ||
ProvisionJobID: uuid.New(), | ||
ProvisionJobID: provisionerJob.ID, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("insert workspace history: %w", err) | ||
|
@@ -132,7 +166,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque | |
} | ||
|
||
render.Status(r, http.StatusCreated) | ||
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory)) | ||
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory, provisionerJob)) | ||
} | ||
|
||
// Returns all workspace history. This is not sorted. Use before/after to chronologically sort. | ||
|
@@ -152,7 +186,14 @@ func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) | |
|
||
apiHistory := make([]WorkspaceHistory, 0, len(histories)) | ||
for _, history := range histories { | ||
apiHistory = append(apiHistory, convertWorkspaceHistory(history)) | ||
job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID) | ||
if err != nil { | ||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ | ||
Message: fmt.Sprintf("get provisioner job: %s", err), | ||
}) | ||
return | ||
} | ||
apiHistory = append(apiHistory, convertWorkspaceHistory(history, job)) | ||
} | ||
|
||
render.Status(r, http.StatusOK) | ||
|
@@ -176,9 +217,33 @@ func (api *api) latestWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Req | |
}) | ||
return | ||
} | ||
job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID) | ||
if err != nil { | ||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ | ||
Message: fmt.Sprintf("get provisioner job: %s", err), | ||
}) | ||
return | ||
} | ||
|
||
render.Status(r, http.StatusOK) | ||
render.JSON(rw, r, convertWorkspaceHistory(history)) | ||
render.JSON(rw, r, convertWorkspaceHistory(history, job)) | ||
} | ||
|
||
// Converts the internal history representation to a public external-facing model. | ||
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory, provisionerJob database.ProvisionerJob) WorkspaceHistory { | ||
//nolint:unconvert | ||
return WorkspaceHistory(WorkspaceHistory{ | ||
ID: workspaceHistory.ID, | ||
CreatedAt: workspaceHistory.CreatedAt, | ||
UpdatedAt: workspaceHistory.UpdatedAt, | ||
WorkspaceID: workspaceHistory.WorkspaceID, | ||
ProjectHistoryID: workspaceHistory.ProjectHistoryID, | ||
BeforeID: workspaceHistory.BeforeID.UUID, | ||
AfterID: workspaceHistory.AfterID.UUID, | ||
Transition: workspaceHistory.Transition, | ||
Initiator: workspaceHistory.Initiator, | ||
Job: convertProvisionerJob(provisionerJob), | ||
}) | ||
} | ||
|
||
func workspaceHistoryLogsChannel(workspaceHistoryID uuid.UUID) string { | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a state for cancelled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose I'm not sure. I felt like failed would represent cancelled, but I suppose that's a different state. I'm going to add it in!