Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 14 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Return jobs with WorkspaceHistory
  • Loading branch information
kylecarbs committed Feb 1, 2022
commit d062b5b6141ab5fbb45a0451b450a48f9e740a78
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"httpmw",
"moby",
"nhooyr",
"nolint",
"nosec",
"oneof",
"protobuf",
Expand All @@ -43,6 +44,7 @@
"retrier",
"sdkproto",
"stretchr",
"unconvert",
"xerrors",
"yamux"
]
Expand Down
4 changes: 0 additions & 4 deletions coderd/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,6 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
ID: workspaceHistory.ID,
UpdatedAt: database.Now(),
ProvisionerState: jobType.WorkspaceProvision.State,
CompletedAt: sql.NullTime{
Time: database.Now(),
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("update workspace history: %w", err)
Expand Down
67 changes: 67 additions & 0 deletions coderd/provisioners.go
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"
Copy link
Contributor

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?

Copy link
Member Author

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!

)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why both CancelledAt and CompletedAt are needed? I'm thinking there could be just FinishedAt - and it would be the completion time on success, and cancel time if cancelled.

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
}
91 changes: 78 additions & 13 deletions coderd/workspacehistory.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coderd

import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand All @@ -22,13 +23,13 @@ type WorkspaceHistory struct {
ID uuid.UUID `json:"id"`
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"`
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 uuid.UUID type, because we have to support v1 users.

Job ProvisionerJob `json:"job"`
}

// CreateWorkspaceHistoryRequest provides options to update the latest workspace history.
Expand All @@ -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) {
Expand All @@ -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{
Expand All @@ -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(),
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
17 changes: 0 additions & 17 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,3 @@ func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace)
}

// Converts the internal history representation to a public external-facing model.
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory {
//nolint:unconvert
return WorkspaceHistory(WorkspaceHistory{
ID: workspaceHistory.ID,
CreatedAt: workspaceHistory.CreatedAt,
UpdatedAt: workspaceHistory.UpdatedAt,
CompletedAt: workspaceHistory.CompletedAt.Time,
WorkspaceID: workspaceHistory.WorkspaceID,
ProjectHistoryID: workspaceHistory.ProjectHistoryID,
BeforeID: workspaceHistory.BeforeID.UUID,
AfterID: workspaceHistory.AfterID.UUID,
Transition: workspaceHistory.Transition,
Initiator: workspaceHistory.Initiator,
})
}
1 change: 0 additions & 1 deletion database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,6 @@ func (q *fakeQuerier) UpdateWorkspaceHistoryByID(_ context.Context, arg database
continue
}
workspaceHistory.UpdatedAt = arg.UpdatedAt
workspaceHistory.CompletedAt = arg.CompletedAt
workspaceHistory.AfterID = arg.AfterID
workspaceHistory.ProvisionerState = arg.ProvisionerState
q.workspaceHistory[index] = workspaceHistory
Expand Down
1 change: 0 additions & 1 deletion database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion database/migrations/000003_workspaces.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ CREATE TABLE workspace_history (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
completed_at timestamptz,
workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE,
project_history_id uuid NOT NULL REFERENCES project_history (id) ON DELETE CASCADE,
name varchar(64) NOT NULL,
Expand Down
1 change: 0 additions & 1 deletion database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions database/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -619,8 +619,7 @@ UPDATE
workspace_history
SET
updated_at = $2,
completed_at = $3,
after_id = $4,
provisioner_state = $5
after_id = $3,
provisioner_state = $4
WHERE
id = $1;
Loading