mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(coderd): add matched provisioner daemons information to more places (#15688)
- Refactors `checkProvisioners` into `db2sdk.MatchedProvisioners` - Adds a separate RBAC subject just for reading provisioner daemons - Adds matched provisioners information to additional endpoints relating to workspace builds and templates -Updates existing unit tests for above endpoints -Adds API endpoint for matched provisioners of template dry-run job -Updates CLI to show warning when creating/starting/stopping/deleting workspaces for which no provisoners are available --------- Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
var (
|
||||
warnNoMatchedProvisioners = `Your build has been enqueued, but there are no provisioners that accept the required tags. Once a compatible provisioner becomes available, your build will continue. Please contact your administrator.
|
||||
Details:
|
||||
Provisioner job ID : %s
|
||||
Requested tags : %s
|
||||
`
|
||||
warnNoAvailableProvisioners = `Provisioners that accept the required tags have not responded for longer than expected. This may delay your build. Please contact your administrator if your build does not complete.
|
||||
Details:
|
||||
Provisioner job ID : %s
|
||||
Requested tags : %s
|
||||
Most recently seen : %s
|
||||
`
|
||||
)
|
||||
|
||||
// WarnMatchedProvisioners warns the user if there are no provisioners that
|
||||
// match the requested tags for a given provisioner job.
|
||||
// If the job is not pending, it is ignored.
|
||||
func WarnMatchedProvisioners(w io.Writer, mp *codersdk.MatchedProvisioners, job codersdk.ProvisionerJob) {
|
||||
if mp == nil {
|
||||
// Nothing in the response, nothing to do here!
|
||||
return
|
||||
}
|
||||
if job.Status != codersdk.ProvisionerJobPending {
|
||||
// Only warn if the job is pending.
|
||||
return
|
||||
}
|
||||
var tagsJSON strings.Builder
|
||||
if err := json.NewEncoder(&tagsJSON).Encode(job.Tags); err != nil {
|
||||
// Fall back to the less-pretty string representation.
|
||||
tagsJSON.Reset()
|
||||
_, _ = tagsJSON.WriteString(fmt.Sprintf("%v", job.Tags))
|
||||
}
|
||||
if mp.Count == 0 {
|
||||
cliui.Warnf(w, warnNoMatchedProvisioners, job.ID, tagsJSON.String())
|
||||
return
|
||||
}
|
||||
if mp.Available == 0 {
|
||||
cliui.Warnf(w, warnNoAvailableProvisioners, job.ID, strings.TrimSpace(tagsJSON.String()), mp.MostRecentlySeen.Time)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cliutil_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestWarnMatchedProvisioners(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mp *codersdk.MatchedProvisioners
|
||||
job codersdk.ProvisionerJob
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "no_match",
|
||||
mp: &codersdk.MatchedProvisioners{
|
||||
Count: 0,
|
||||
Available: 0,
|
||||
},
|
||||
job: codersdk.ProvisionerJob{
|
||||
Status: codersdk.ProvisionerJobPending,
|
||||
},
|
||||
expect: `there are no provisioners that accept the required tags`,
|
||||
},
|
||||
{
|
||||
name: "no_available",
|
||||
mp: &codersdk.MatchedProvisioners{
|
||||
Count: 1,
|
||||
Available: 0,
|
||||
},
|
||||
job: codersdk.ProvisionerJob{
|
||||
Status: codersdk.ProvisionerJobPending,
|
||||
},
|
||||
expect: `Provisioners that accept the required tags have not responded for longer than expected`,
|
||||
},
|
||||
{
|
||||
name: "match",
|
||||
mp: &codersdk.MatchedProvisioners{
|
||||
Count: 1,
|
||||
Available: 1,
|
||||
},
|
||||
job: codersdk.ProvisionerJob{
|
||||
Status: codersdk.ProvisionerJobPending,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not_pending",
|
||||
mp: &codersdk.MatchedProvisioners{},
|
||||
job: codersdk.ProvisionerJob{
|
||||
Status: codersdk.ProvisionerJobRunning,
|
||||
},
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var w strings.Builder
|
||||
cliutil.WarnMatchedProvisioners(&w, tt.mp, tt.job)
|
||||
if tt.expect != "" {
|
||||
require.Contains(t, w.String(), tt.expect)
|
||||
} else {
|
||||
require.Empty(t, w.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/coder/pretty"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -289,7 +290,7 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
|
||||
}
|
||||
|
||||
workspace, err := client.CreateWorkspace(inv.Context(), template.OrganizationID, workspaceOwner, codersdk.CreateWorkspaceRequest{
|
||||
workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, codersdk.CreateWorkspaceRequest{
|
||||
TemplateVersionID: templateVersionID,
|
||||
Name: workspaceName,
|
||||
AutostartSchedule: schedSpec,
|
||||
@@ -301,6 +302,8 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
return xerrors.Errorf("create workspace: %w", err)
|
||||
}
|
||||
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
|
||||
|
||||
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, workspace.LatestBuild.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("watch build: %w", err)
|
||||
@@ -433,6 +436,12 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
|
||||
}
|
||||
|
||||
matchedProvisioners, err := client.TemplateVersionDryRunMatchedProvisioners(inv.Context(), templateVersion.ID, dryRun.ID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get matched provisioners: %w", err)
|
||||
}
|
||||
cliutil.WarnMatchedProvisioners(inv.Stdout, &matchedProvisioners, dryRun)
|
||||
_, _ = fmt.Fprintln(inv.Stdout, "Planning workspace...")
|
||||
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
@@ -55,6 +56,7 @@ func (r *RootCmd) deleteWorkspace() *serpent.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliutil.WarnMatchedProvisioners(inv.Stdout, build.MatchedProvisioners, build.Job)
|
||||
|
||||
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/pty/ptytest"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
@@ -164,4 +166,46 @@ func TestDelete(t *testing.T) {
|
||||
}()
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("WarnNoProvisioners", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test requires postgres")
|
||||
}
|
||||
|
||||
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
||||
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
|
||||
// Given: a user, template, and workspace
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
version := coderdtest.CreateTemplateVersion(t, templateAdmin, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, templateAdmin, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, templateAdmin, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, workspace.LatestBuild.ID)
|
||||
|
||||
// When: all provisioner daemons disappear
|
||||
require.NoError(t, closeDaemon.Close())
|
||||
_, err := db.Exec("DELETE FROM provisioner_daemons;")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the workspace deletion should warn about no provisioners
|
||||
inv, root := clitest.New(t, "delete", workspace.Name, "-y")
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
doneChan := make(chan struct{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
_ = inv.WithContext(ctx).Run()
|
||||
}()
|
||||
pty.ExpectMatch("there are no provisioners that accept the required tags")
|
||||
cancel()
|
||||
<-doneChan
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
@@ -35,6 +36,23 @@ func (r *RootCmd) start() *serpent.Command {
|
||||
}
|
||||
var build codersdk.WorkspaceBuild
|
||||
switch workspace.LatestBuild.Status {
|
||||
case codersdk.WorkspaceStatusPending:
|
||||
// The above check is technically duplicated in cliutil.WarnmatchedProvisioners
|
||||
// but we still want to avoid users spamming multiple builds that will
|
||||
// not be picked up.
|
||||
_, _ = fmt.Fprintf(
|
||||
inv.Stdout,
|
||||
"\nThe %s workspace is waiting to start!\n",
|
||||
cliui.Keyword(workspace.Name),
|
||||
)
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
|
||||
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Enqueue another start?",
|
||||
IsConfirm: true,
|
||||
Default: cliui.ConfirmNo,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case codersdk.WorkspaceStatusRunning:
|
||||
_, _ = fmt.Fprintf(
|
||||
inv.Stdout, "\nThe %s workspace is already running!\n",
|
||||
@@ -159,6 +177,7 @@ func startWorkspace(inv *serpent.Invocation, client *codersdk.Client, workspace
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceBuild{}, xerrors.Errorf("create workspace build: %w", err)
|
||||
}
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
|
||||
|
||||
return build, nil
|
||||
}
|
||||
|
||||
+17
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
@@ -36,6 +37,21 @@ func (r *RootCmd) stop() *serpent.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobPending {
|
||||
// cliutil.WarnMatchedProvisioners also checks if the job is pending
|
||||
// but we still want to avoid users spamming multiple builds that will
|
||||
// not be picked up.
|
||||
cliui.Warn(inv.Stderr, "The workspace is already stopping!")
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
|
||||
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Enqueue another stop?",
|
||||
IsConfirm: true,
|
||||
Default: cliui.ConfirmNo,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
wbr := codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: codersdk.WorkspaceTransitionStop,
|
||||
}
|
||||
@@ -46,6 +62,7 @@ func (r *RootCmd) stop() *serpent.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
|
||||
|
||||
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
|
||||
if err != nil {
|
||||
|
||||
+2
-37
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/pretty"
|
||||
@@ -416,7 +416,7 @@ func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplat
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
WarnMatchedProvisioners(inv, version)
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, version.MatchedProvisioners, version.Job)
|
||||
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
version, err := client.TemplateVersion(inv.Context(), version.ID)
|
||||
@@ -482,41 +482,6 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
var (
|
||||
warnNoMatchedProvisioners = `Your build has been enqueued, but there are no provisioners that accept the required tags. Once a compatible provisioner becomes available, your build will continue. Please contact your administrator.
|
||||
Details:
|
||||
Provisioner job ID : %s
|
||||
Requested tags : %s
|
||||
`
|
||||
warnNoAvailableProvisioners = `Provisioners that accept the required tags have not responded for longer than expected. This may delay your build. Please contact your administrator if your build does not complete.
|
||||
Details:
|
||||
Provisioner job ID : %s
|
||||
Requested tags : %s
|
||||
Most recently seen : %s
|
||||
`
|
||||
)
|
||||
|
||||
func WarnMatchedProvisioners(inv *serpent.Invocation, tv codersdk.TemplateVersion) {
|
||||
if tv.MatchedProvisioners == nil {
|
||||
// Nothing in the response, nothing to do here!
|
||||
return
|
||||
}
|
||||
var tagsJSON strings.Builder
|
||||
if err := json.NewEncoder(&tagsJSON).Encode(tv.Job.Tags); err != nil {
|
||||
// Fall back to the less-pretty string representation.
|
||||
tagsJSON.Reset()
|
||||
_, _ = tagsJSON.WriteString(fmt.Sprintf("%v", tv.Job.Tags))
|
||||
}
|
||||
if tv.MatchedProvisioners.Count == 0 {
|
||||
cliui.Warnf(inv.Stderr, warnNoMatchedProvisioners, tv.Job.ID, tagsJSON.String())
|
||||
return
|
||||
}
|
||||
if tv.MatchedProvisioners.Available == 0 {
|
||||
cliui.Warnf(inv.Stderr, warnNoAvailableProvisioners, tv.Job.ID, strings.TrimSpace(tagsJSON.String()), tv.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// prettyDirectoryPath returns a prettified path when inside the users
|
||||
// home directory. Falls back to dir if the users home directory cannot
|
||||
// discerned. This function calls filepath.Clean on the result.
|
||||
|
||||
Generated
+46
@@ -4851,6 +4851,49 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Get template version dry-run matched provisioners",
|
||||
"operationId": "get-template-version-dry-run-matched-provisioners",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Job ID",
|
||||
"name": "jobID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.MatchedProvisioners"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dry-run/{jobID}/resources": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -15068,6 +15111,9 @@ const docTemplate = `{
|
||||
"job": {
|
||||
"$ref": "#/definitions/codersdk.ProvisionerJob"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"$ref": "#/definitions/codersdk.MatchedProvisioners"
|
||||
},
|
||||
"max_deadline": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
||||
Generated
+42
@@ -4275,6 +4275,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Get template version dry-run matched provisioners",
|
||||
"operationId": "get-template-version-dry-run-matched-provisioners",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Job ID",
|
||||
"name": "jobID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.MatchedProvisioners"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dry-run/{jobID}/resources": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -13712,6 +13751,9 @@
|
||||
"job": {
|
||||
"$ref": "#/definitions/codersdk.ProvisionerJob"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"$ref": "#/definitions/codersdk.MatchedProvisioners"
|
||||
},
|
||||
"max_deadline": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
||||
@@ -245,7 +245,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
}
|
||||
}
|
||||
|
||||
nextBuild, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
|
||||
nextBuild, job, _, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("build workspace with transition %q: %w", nextTransition, err)
|
||||
}
|
||||
|
||||
@@ -1055,6 +1055,7 @@ func New(options *Options) *API {
|
||||
r.Get("/{jobID}", api.templateVersionDryRun)
|
||||
r.Get("/{jobID}/resources", api.templateVersionDryRunResources)
|
||||
r.Get("/{jobID}/logs", api.templateVersionDryRunLogs)
|
||||
r.Get("/{jobID}/matched-provisioners", api.templateVersionDryRunMatchedProvisioners)
|
||||
r.Patch("/{jobID}/cancel", api.patchTemplateVersionDryRunCancel)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -673,3 +673,23 @@ func CryptoKey(key database.CryptoKey) codersdk.CryptoKey {
|
||||
Secret: key.Secret.String,
|
||||
}
|
||||
}
|
||||
|
||||
func MatchedProvisioners(provisionerDaemons []database.ProvisionerDaemon, now time.Time, staleInterval time.Duration) codersdk.MatchedProvisioners {
|
||||
minLastSeenAt := now.Add(-staleInterval)
|
||||
mostRecentlySeen := codersdk.NullTime{}
|
||||
var matched codersdk.MatchedProvisioners
|
||||
for _, provisioner := range provisionerDaemons {
|
||||
if !provisioner.LastSeenAt.Valid {
|
||||
continue
|
||||
}
|
||||
matched.Count++
|
||||
if provisioner.LastSeenAt.Time.After(minLastSeenAt) {
|
||||
matched.Available++
|
||||
}
|
||||
if provisioner.LastSeenAt.Time.After(mostRecentlySeen.Time) {
|
||||
matched.MostRecentlySeen.Valid = true
|
||||
matched.MostRecentlySeen.Time = provisioner.LastSeenAt.Time
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ var (
|
||||
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
|
||||
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
|
||||
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead},
|
||||
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
|
||||
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||
rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
|
||||
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
|
||||
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
|
||||
@@ -317,6 +317,23 @@ var (
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
subjectSystemReadProvisionerDaemons = rbac.Subject{
|
||||
FriendlyName: "Provisioner Daemons Reader",
|
||||
ID: uuid.Nil.String(),
|
||||
Roles: rbac.Roles([]rbac.Role{
|
||||
{
|
||||
Identifier: rbac.RoleIdentifier{Name: "system-read-provisioner-daemons"},
|
||||
DisplayName: "Coder",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceProvisionerDaemon.Type: {policy.ActionRead},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
},
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
)
|
||||
|
||||
// AsProvisionerd returns a context with an actor that has permissions required
|
||||
@@ -359,6 +376,12 @@ func AsSystemRestricted(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted)
|
||||
}
|
||||
|
||||
// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
|
||||
// to read provisioner daemons.
|
||||
func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons)
|
||||
}
|
||||
|
||||
var AsRemoveActor = rbac.Subject{
|
||||
ID: "remove-actor",
|
||||
}
|
||||
|
||||
+154
-58
@@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
@@ -22,6 +21,8 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/render"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/examples"
|
||||
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
|
||||
@@ -60,6 +62,22 @@ func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var matchedProvisioners *codersdk.MatchedProvisioners
|
||||
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
|
||||
// nolint: gocritic // The user hitting this endpoint may not have
|
||||
// permission to read provisioner daemons, but we want to show them
|
||||
// information about the provisioner daemons that are available.
|
||||
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
|
||||
WantTags: jobs[0].ProvisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
|
||||
} else {
|
||||
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
|
||||
}
|
||||
}
|
||||
|
||||
schemas, err := api.Database.GetParameterSchemasByJobID(ctx, jobs[0].ProvisionerJob.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
@@ -77,7 +95,7 @@ func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
warnings = append(warnings, codersdk.TemplateVersionWarningUnsupportedWorkspaces)
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil, warnings))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, warnings))
|
||||
}
|
||||
|
||||
// @Summary Patch template version by ID
|
||||
@@ -173,7 +191,23 @@ func (api *API) patchTemplateVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), nil, nil))
|
||||
var matchedProvisioners *codersdk.MatchedProvisioners
|
||||
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
|
||||
// nolint: gocritic // The user hitting this endpoint may not have
|
||||
// permission to read provisioner daemons, but we want to show them
|
||||
// information about the provisioner daemons that are available.
|
||||
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
|
||||
WantTags: jobs[0].ProvisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
|
||||
} else {
|
||||
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
|
||||
}
|
||||
|
||||
// @Summary Cancel template version by ID
|
||||
@@ -546,6 +580,43 @@ func (api *API) templateVersionDryRun(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerJob(job))
|
||||
}
|
||||
|
||||
// @Summary Get template version dry-run matched provisioners
|
||||
// @ID get-template-version-dry-run-matched-provisioners
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Param jobID path string true "Job ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.MatchedProvisioners
|
||||
// @Router /templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners [get]
|
||||
func (api *API) templateVersionDryRunMatchedProvisioners(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
job, ok := api.fetchTemplateVersionDryRunJob(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// nolint:gocritic // The user may not have permissions to read all
|
||||
// provisioner daemons in the org.
|
||||
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: job.ProvisionerJob.OrganizationID,
|
||||
WantTags: job.ProvisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching provisioner daemons by organization.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
daemons = []database.ProvisionerDaemon{}
|
||||
}
|
||||
|
||||
matchedProvisioners := db2sdk.MatchedProvisioners(daemons, dbtime.Now(), provisionerdserver.StaleInterval)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, matchedProvisioners)
|
||||
}
|
||||
|
||||
// @Summary Get template version dry-run resources by job ID
|
||||
// @ID get-template-version-dry-run-resources-by-job-id
|
||||
// @Security CoderSessionToken
|
||||
@@ -868,8 +939,23 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
var matchedProvisioners *codersdk.MatchedProvisioners
|
||||
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
|
||||
// nolint: gocritic // The user hitting this endpoint may not have
|
||||
// permission to read provisioner daemons, but we want to show them
|
||||
// information about the provisioner daemons that are available.
|
||||
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
|
||||
WantTags: jobs[0].ProvisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
|
||||
} else {
|
||||
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil, nil))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
|
||||
}
|
||||
|
||||
// @Summary Get template version by organization, template, and name
|
||||
@@ -934,7 +1020,23 @@ func (api *API) templateVersionByOrganizationTemplateAndName(rw http.ResponseWri
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil, nil))
|
||||
var matchedProvisioners *codersdk.MatchedProvisioners
|
||||
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
|
||||
// nolint: gocritic // The user hitting this endpoint may not have
|
||||
// permission to read provisioner daemons, but we want to show them
|
||||
// information about the provisioner daemons that are available.
|
||||
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
|
||||
WantTags: jobs[0].ProvisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
|
||||
} else {
|
||||
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
|
||||
}
|
||||
|
||||
// @Summary Get previous template version by organization, template, and name
|
||||
@@ -1020,7 +1122,23 @@ func (api *API) previousTemplateVersionByOrganizationTemplateAndName(rw http.Res
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), nil, nil))
|
||||
var matchedProvisioners *codersdk.MatchedProvisioners
|
||||
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
|
||||
// nolint: gocritic // The user hitting this endpoint may not have
|
||||
// permission to read provisioner daemons, but we want to show them
|
||||
// information about the provisioner daemons that are available.
|
||||
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
|
||||
WantTags: jobs[0].ProvisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
|
||||
} else {
|
||||
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
|
||||
}
|
||||
|
||||
// @Summary Archive template unused versions by template id
|
||||
@@ -1513,27 +1631,6 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for eligible provisioners. This allows us to log a message warning deployment administrators
|
||||
// of users submitting jobs for which no provisioners are available.
|
||||
matchedProvisioners, err = checkProvisioners(ctx, tx, organization.ID, tags)
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to check eligible provisioner daemons for job", slog.Error(err))
|
||||
} else if matchedProvisioners.Count == 0 {
|
||||
api.Logger.Warn(ctx, "no matching provisioners found for job",
|
||||
slog.F("user_id", apiKey.UserID),
|
||||
slog.F("job_id", jobID),
|
||||
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
|
||||
slog.F("tags", tags),
|
||||
)
|
||||
} else if matchedProvisioners.Available == 0 {
|
||||
api.Logger.Warn(ctx, "no active provisioners found for job",
|
||||
slog.F("user_id", apiKey.UserID),
|
||||
slog.F("job_id", jobID),
|
||||
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
|
||||
slog.F("tags", tags),
|
||||
)
|
||||
}
|
||||
|
||||
provisionerJob, err = tx.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
|
||||
ID: jobID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
@@ -1559,6 +1656,36 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for eligible provisioners. This allows us to return a warning to the user if they
|
||||
// submit a job for which no provisioner is available.
|
||||
// nolint: gocritic // The user hitting this endpoint may not have
|
||||
// permission to read provisioner daemons, but we want to show them
|
||||
// information about the provisioner daemons that are available.
|
||||
eligibleProvisioners, err := tx.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: organization.ID,
|
||||
WantTags: provisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
// Log the error but do not return any warnings. This is purely advisory and we should not block.
|
||||
api.Logger.Error(ctx, "failed to check eligible provisioner daemons for job", slog.Error(err))
|
||||
}
|
||||
matchedProvisioners = db2sdk.MatchedProvisioners(eligibleProvisioners, provisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
if matchedProvisioners.Count == 0 {
|
||||
api.Logger.Warn(ctx, "no matching provisioners found for job",
|
||||
slog.F("user_id", apiKey.UserID),
|
||||
slog.F("job_id", jobID),
|
||||
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
|
||||
slog.F("tags", tags),
|
||||
)
|
||||
} else if matchedProvisioners.Available == 0 {
|
||||
api.Logger.Warn(ctx, "no active provisioners found for job",
|
||||
slog.F("user_id", apiKey.UserID),
|
||||
slog.F("job_id", jobID),
|
||||
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
|
||||
slog.F("tags", tags),
|
||||
)
|
||||
}
|
||||
|
||||
var templateID uuid.NullUUID
|
||||
if req.TemplateID != uuid.Nil {
|
||||
templateID = uuid.NullUUID{
|
||||
@@ -1822,34 +1949,3 @@ func (api *API) publishTemplateUpdate(ctx context.Context, templateID uuid.UUID)
|
||||
slog.F("template_id", templateID), slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func checkProvisioners(ctx context.Context, store database.Store, orgID uuid.UUID, wantTags map[string]string) (codersdk.MatchedProvisioners, error) {
|
||||
// Check for eligible provisioners. This allows us to return a warning to the user if they
|
||||
// submit a job for which no provisioner is available.
|
||||
eligibleProvisioners, err := store.GetProvisionerDaemonsByOrganization(ctx, database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: orgID,
|
||||
WantTags: wantTags,
|
||||
})
|
||||
if err != nil {
|
||||
// Log the error but do not return any warnings. This is purely advisory and we should not block.
|
||||
return codersdk.MatchedProvisioners{}, xerrors.Errorf("provisioner daemons by organization: %w", err)
|
||||
}
|
||||
|
||||
staleInterval := time.Now().Add(-provisionerdserver.StaleInterval)
|
||||
mostRecentlySeen := codersdk.NullTime{}
|
||||
var matched codersdk.MatchedProvisioners
|
||||
for _, provisioner := range eligibleProvisioners {
|
||||
if !provisioner.LastSeenAt.Valid {
|
||||
continue
|
||||
}
|
||||
matched.Count++
|
||||
if provisioner.LastSeenAt.Time.After(staleInterval) {
|
||||
matched.Available++
|
||||
}
|
||||
if provisioner.LastSeenAt.Time.After(mostRecentlySeen.Time) {
|
||||
matched.MostRecentlySeen.Valid = true
|
||||
matched.MostRecentlySeen.Time = provisioner.LastSeenAt.Time
|
||||
}
|
||||
}
|
||||
return matched, nil
|
||||
}
|
||||
|
||||
@@ -50,6 +50,12 @@ func TestTemplateVersion(t *testing.T) {
|
||||
tv, err := client.TemplateVersion(ctx, version.ID)
|
||||
authz.AssertChecked(t, policy.ActionRead, tv)
|
||||
require.NoError(t, err)
|
||||
if assert.Equal(t, tv.Job.Status, codersdk.ProvisionerJobPending) {
|
||||
assert.NotNil(t, tv.MatchedProvisioners)
|
||||
assert.Zero(t, tv.MatchedProvisioners.Available)
|
||||
assert.Zero(t, tv.MatchedProvisioners.Count)
|
||||
assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
|
||||
assert.Equal(t, "bananas", tv.Name)
|
||||
assert.Equal(t, "first try", tv.Message)
|
||||
@@ -87,8 +93,14 @@ func TestTemplateVersion(t *testing.T) {
|
||||
|
||||
client1, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
|
||||
_, err := client1.TemplateVersion(ctx, version.ID)
|
||||
tv, err := client1.TemplateVersion(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
if assert.Equal(t, tv.Job.Status, codersdk.ProvisionerJobPending) {
|
||||
assert.NotNil(t, tv.MatchedProvisioners)
|
||||
assert.Zero(t, tv.MatchedProvisioners.Available)
|
||||
assert.Zero(t, tv.MatchedProvisioners.Count)
|
||||
assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,6 +170,12 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "bananas", version.Name)
|
||||
require.Equal(t, provisionersdk.ScopeOrganization, version.Job.Tags[provisionersdk.TagScope])
|
||||
if assert.Equal(t, version.Job.Status, codersdk.ProvisionerJobPending) {
|
||||
assert.NotNil(t, version.MatchedProvisioners)
|
||||
assert.Equal(t, version.MatchedProvisioners.Available, 1)
|
||||
assert.Equal(t, version.MatchedProvisioners.Count, 1)
|
||||
assert.True(t, version.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
|
||||
require.Len(t, auditor.AuditLogs(), 2)
|
||||
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[1].Action)
|
||||
@@ -790,8 +808,15 @@ func TestTemplateVersionByName(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.TemplateVersionByName(ctx, template.ID, version.Name)
|
||||
tv, err := client.TemplateVersionByName(ctx, template.ID, version.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
if assert.Equal(t, tv.Job.Status, codersdk.ProvisionerJobPending) {
|
||||
assert.NotNil(t, tv.MatchedProvisioners)
|
||||
assert.Zero(t, tv.MatchedProvisioners.Available)
|
||||
assert.Zero(t, tv.MatchedProvisioners.Count)
|
||||
assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -979,6 +1004,13 @@ func TestTemplateVersionDryRun(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, job.ID, newJob.ID)
|
||||
|
||||
// Check matched provisioners
|
||||
matched, err := client.TemplateVersionDryRunMatchedProvisioners(ctx, version.ID, job.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, matched.Count)
|
||||
require.Equal(t, 1, matched.Available)
|
||||
require.NotZero(t, matched.MostRecentlySeen.Time)
|
||||
|
||||
// Stream logs
|
||||
logs, closer, err := client.TemplateVersionDryRunLogsAfter(ctx, version.ID, job.ID, 0)
|
||||
require.NoError(t, err)
|
||||
@@ -1151,6 +1183,49 @@ func TestTemplateVersionDryRun(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Pending", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test requires postgres")
|
||||
}
|
||||
|
||||
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
||||
client, closer := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
defer closer.Close()
|
||||
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
})
|
||||
version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
require.Equal(t, codersdk.ProvisionerJobSucceeded, version.Job.Status)
|
||||
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
_, err := db.Exec("DELETE FROM provisioner_daemons")
|
||||
require.NoError(t, err)
|
||||
|
||||
job, err := templateAdmin.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{
|
||||
WorkspaceName: "test",
|
||||
RichParameterValues: []codersdk.WorkspaceBuildParameter{},
|
||||
UserVariableValues: []codersdk.VariableValue{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.ProvisionerJobPending, job.Status)
|
||||
|
||||
matched, err := templateAdmin.TemplateVersionDryRunMatchedProvisioners(ctx, version.ID, job.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, matched.Count)
|
||||
require.Equal(t, 0, matched.Available)
|
||||
require.Zero(t, matched.MostRecentlySeen.Time)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPaginatedTemplateVersions creates a list of template versions and paginate.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/wsbuilder"
|
||||
@@ -85,6 +86,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
data.templateVersions[0],
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -289,6 +291,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
data.templateVersions[0],
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -352,7 +355,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
builder = builder.State(createBuild.ProvisionerState)
|
||||
}
|
||||
|
||||
workspaceBuild, provisionerJob, err := builder.Build(
|
||||
workspaceBuild, provisionerJob, provisionerDaemons, err := builder.Build(
|
||||
ctx,
|
||||
api.Database,
|
||||
func(action policy.Action, object rbac.Objecter) bool {
|
||||
@@ -384,12 +387,18 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
err = provisionerjobs.PostJob(api.Pubsub, *provisionerJob)
|
||||
if err != nil {
|
||||
// Client probably doesn't care about this error, so just log it.
|
||||
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
|
||||
|
||||
if provisionerJob != nil {
|
||||
if err := provisionerjobs.PostJob(api.Pubsub, *provisionerJob); err != nil {
|
||||
// Client probably doesn't care about this error, so just log it.
|
||||
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
var matchedProvisioners codersdk.MatchedProvisioners
|
||||
if provisionerJob != nil {
|
||||
matchedProvisioners = db2sdk.MatchedProvisioners(provisionerDaemons, provisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
}
|
||||
apiBuild, err := api.convertWorkspaceBuild(
|
||||
*workspaceBuild,
|
||||
workspace,
|
||||
@@ -404,6 +413,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
[]database.WorkspaceAgentScript{},
|
||||
[]database.WorkspaceAgentLogSource{},
|
||||
database.TemplateVersion{},
|
||||
&matchedProvisioners,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -804,6 +814,7 @@ func (api *API) convertWorkspaceBuilds(
|
||||
agentScripts,
|
||||
agentLogSources,
|
||||
templateVersion,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("converting workspace build: %w", err)
|
||||
@@ -826,6 +837,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
agentScripts []database.WorkspaceAgentScript,
|
||||
agentLogSources []database.WorkspaceAgentLogSource,
|
||||
templateVersion database.TemplateVersion,
|
||||
matchedProvisioners *codersdk.MatchedProvisioners,
|
||||
) (codersdk.WorkspaceBuild, error) {
|
||||
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
|
||||
for _, resource := range workspaceResources {
|
||||
@@ -918,6 +930,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
Resources: apiResources,
|
||||
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
|
||||
DailyCost: build.DailyCost,
|
||||
MatchedProvisioners: matchedProvisioners,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1097,6 +1097,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
Transition: codersdk.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if assert.NotNil(t, build.MatchedProvisioners) {
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
||||
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
}
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
@@ -1124,6 +1130,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
Transition: codersdk.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if assert.NotNil(t, build.MatchedProvisioners) {
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
||||
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
}
|
||||
|
||||
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
|
||||
})
|
||||
|
||||
@@ -1150,6 +1162,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
ProvisionerState: wantState,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if assert.NotNil(t, build.MatchedProvisioners) {
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
||||
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
}
|
||||
|
||||
gotState, err := client.WorkspaceBuildState(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantState, gotState)
|
||||
@@ -1173,6 +1191,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
|
||||
if assert.NotNil(t, build.MatchedProvisioners) {
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
||||
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
||||
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
}
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
@@ -1181,6 +1205,102 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Workspaces, 0)
|
||||
})
|
||||
|
||||
t.Run("NoProvisionersAvailable", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test requires postgres")
|
||||
}
|
||||
// Given: a coderd instance with a provisioner daemon
|
||||
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
||||
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
defer closeDaemon.Close()
|
||||
// Given: a user, template, and workspace
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Stop the provisioner daemon.
|
||||
require.NoError(t, closeDaemon.Close())
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
// Given: no provisioner daemons exist.
|
||||
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: a new workspace build is created
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Transition: codersdk.WorkspaceTransitionStart,
|
||||
})
|
||||
// Then: the request should succeed.
|
||||
require.NoError(t, err)
|
||||
// Then: the provisioner job should remain pending.
|
||||
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)
|
||||
// Then: the response should indicate no provisioners are available.
|
||||
if assert.NotNil(t, build.MatchedProvisioners) {
|
||||
assert.Zero(t, build.MatchedProvisioners.Count)
|
||||
assert.Zero(t, build.MatchedProvisioners.Available)
|
||||
assert.Zero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
assert.False(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AllProvisionersStale", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test requires postgres")
|
||||
}
|
||||
// Given: a coderd instance with a provisioner daemon
|
||||
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
||||
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
defer closeDaemon.Close()
|
||||
// Given: a user, template, and workspace
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
// Given: all provisioner daemons are stale
|
||||
// First stop the provisioner
|
||||
require.NoError(t, closeDaemon.Close())
|
||||
newLastSeenAt := dbtime.Now().Add(-time.Hour)
|
||||
// Update the last seen at for all provisioner daemons. We have to use the
|
||||
// SQL db directly because store.UpdateProvisionerDaemonLastSeenAt has a
|
||||
// built-in check to prevent updating the last seen at to a time in the past.
|
||||
_, err := db.ExecContext(ctx, `UPDATE provisioner_daemons SET last_seen_at = $1;`, newLastSeenAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: a new workspace build is created
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Transition: codersdk.WorkspaceTransitionStart,
|
||||
})
|
||||
// Then: the request should succeed
|
||||
require.NoError(t, err)
|
||||
// Then: the provisioner job should remain pending
|
||||
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)
|
||||
// Then: the response should indicate no provisioners are available
|
||||
if assert.NotNil(t, build.MatchedProvisioners) {
|
||||
assert.Zero(t, build.MatchedProvisioners.Available)
|
||||
assert.Equal(t, 1, build.MatchedProvisioners.Count)
|
||||
assert.Equal(t, newLastSeenAt.UTC(), build.MatchedProvisioners.MostRecentlySeen.Time.UTC())
|
||||
assert.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
|
||||
+13
-5
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
@@ -593,8 +594,7 @@ func createWorkspace(
|
||||
}},
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: fmt.Sprintf("Internal error fetching workspace by name %q.", req.Name),
|
||||
Detail: err.Error(),
|
||||
@@ -603,8 +603,10 @@ func createWorkspace(
|
||||
}
|
||||
|
||||
var (
|
||||
provisionerJob *database.ProvisionerJob
|
||||
workspaceBuild *database.WorkspaceBuild
|
||||
provisionerJob *database.ProvisionerJob
|
||||
workspaceBuild *database.WorkspaceBuild
|
||||
provisionerDaemons []database.ProvisionerDaemon
|
||||
matchedProvisioners codersdk.MatchedProvisioners
|
||||
)
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
now := dbtime.Now()
|
||||
@@ -645,7 +647,7 @@ func createWorkspace(
|
||||
builder = builder.VersionID(req.TemplateVersionID)
|
||||
}
|
||||
|
||||
workspaceBuild, provisionerJob, err = builder.Build(
|
||||
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
|
||||
ctx,
|
||||
db,
|
||||
func(action policy.Action, object rbac.Objecter) bool {
|
||||
@@ -655,6 +657,7 @@ func createWorkspace(
|
||||
)
|
||||
return err
|
||||
}, nil)
|
||||
|
||||
var bldErr wsbuilder.BuildError
|
||||
if xerrors.As(err, &bldErr) {
|
||||
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
|
||||
@@ -675,6 +678,10 @@ func createWorkspace(
|
||||
// Client probably doesn't care about this error, so just log it.
|
||||
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
|
||||
}
|
||||
if provisionerJob != nil {
|
||||
matchedProvisioners = db2sdk.MatchedProvisioners(provisionerDaemons, provisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
}
|
||||
|
||||
auditReq.New = workspace.WorkspaceTable()
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
@@ -696,6 +703,7 @@ func createWorkspace(
|
||||
[]database.WorkspaceAgentScript{},
|
||||
[]database.WorkspaceAgentLogSource{},
|
||||
database.TemplateVersion{},
|
||||
&matchedProvisioners,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
|
||||
@@ -766,6 +766,94 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, exp, *ws.TTLMillis)
|
||||
})
|
||||
|
||||
t.Run("NoProvisionersAvailable", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test requires postgres")
|
||||
}
|
||||
// Given: a coderd instance with a provisioner daemon
|
||||
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
||||
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
defer closeDaemon.Close()
|
||||
|
||||
// Given: a user, template, and workspace
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
// Given: all the provisioner daemons disappear
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: a new workspace is created
|
||||
ws, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: "testing",
|
||||
})
|
||||
// Then: the request succeeds
|
||||
require.NoError(t, err)
|
||||
// Then: the workspace build is pending
|
||||
require.Equal(t, codersdk.ProvisionerJobPending, ws.LatestBuild.Job.Status)
|
||||
// Then: the workspace build has no matched provisioners
|
||||
if assert.NotNil(t, ws.LatestBuild.MatchedProvisioners) {
|
||||
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Count)
|
||||
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Available)
|
||||
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Time)
|
||||
assert.False(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AllProvisionersStale", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test requires postgres")
|
||||
}
|
||||
|
||||
// Given: a coderd instance with a provisioner daemon
|
||||
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
||||
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
defer closeDaemon.Close()
|
||||
|
||||
// Given: a user, template, and workspace
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
// Given: all the provisioner daemons have not been seen for a while
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
newLastSeenAt := dbtime.Now().Add(-time.Hour)
|
||||
_, err := db.ExecContext(ctx, `UPDATE provisioner_daemons SET last_seen_at = $1;`, newLastSeenAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: a new workspace is created
|
||||
ws, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: "testing",
|
||||
})
|
||||
// Then: the request succeeds
|
||||
require.NoError(t, err)
|
||||
// Then: the workspace build is pending
|
||||
require.Equal(t, codersdk.ProvisionerJobPending, ws.LatestBuild.Job.Status)
|
||||
// Then: we can see that there are some provisioners that are stale
|
||||
if assert.NotNil(t, ws.LatestBuild.MatchedProvisioners) {
|
||||
assert.Equal(t, 1, ws.LatestBuild.MatchedProvisioners.Count)
|
||||
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Available)
|
||||
assert.Equal(t, newLastSeenAt.UTC(), ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Time.UTC())
|
||||
assert.True(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceByOwnerAndName(t *testing.T) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
@@ -213,12 +214,12 @@ func (b *Builder) Build(
|
||||
authFunc func(action policy.Action, object rbac.Objecter) bool,
|
||||
auditBaggage audit.WorkspaceBuildBaggage,
|
||||
) (
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, error,
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.ProvisionerDaemon, error,
|
||||
) {
|
||||
var err error
|
||||
b.ctx, err = audit.BaggageToContext(ctx, auditBaggage)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("create audit baggage: %w", err)
|
||||
return nil, nil, nil, xerrors.Errorf("create audit baggage: %w", err)
|
||||
}
|
||||
|
||||
// Run the build in a transaction with RepeatableRead isolation, and retries.
|
||||
@@ -227,16 +228,17 @@ func (b *Builder) Build(
|
||||
// later reads are consistent with earlier ones.
|
||||
var workspaceBuild *database.WorkspaceBuild
|
||||
var provisionerJob *database.ProvisionerJob
|
||||
var provisionerDaemons []database.ProvisionerDaemon
|
||||
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
|
||||
var err error
|
||||
b.store = tx
|
||||
workspaceBuild, provisionerJob, err = b.buildTx(authFunc)
|
||||
workspaceBuild, provisionerJob, provisionerDaemons, err = b.buildTx(authFunc)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("build tx: %w", err)
|
||||
return nil, nil, nil, xerrors.Errorf("build tx: %w", err)
|
||||
}
|
||||
return workspaceBuild, provisionerJob, nil
|
||||
return workspaceBuild, provisionerJob, provisionerDaemons, nil
|
||||
}
|
||||
|
||||
// buildTx contains the business logic of computing a new build. Attributes of the new database objects are computed
|
||||
@@ -246,35 +248,35 @@ func (b *Builder) Build(
|
||||
//
|
||||
// In order to utilize this cache, the functions that compute build attributes use a pointer receiver type.
|
||||
func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Objecter) bool) (
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, error,
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.ProvisionerDaemon, error,
|
||||
) {
|
||||
if authFunc != nil {
|
||||
err := b.authorize(authFunc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
err := b.checkTemplateVersionMatchesTemplate()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
err = b.checkTemplateJobStatus()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
err = b.checkRunningBuild()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
template, err := b.getTemplate()
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch template", err}
|
||||
return nil, nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch template", err}
|
||||
}
|
||||
|
||||
templateVersionJob, err := b.getTemplateVersionJob()
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{
|
||||
return nil, nil, nil, BuildError{
|
||||
http.StatusInternalServerError, "failed to fetch template version job", err,
|
||||
}
|
||||
}
|
||||
@@ -294,7 +296,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
|
||||
LogLevel: b.logLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{
|
||||
return nil, nil, nil, BuildError{
|
||||
http.StatusInternalServerError,
|
||||
"marshal provision job",
|
||||
err,
|
||||
@@ -302,12 +304,12 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
|
||||
}
|
||||
traceMetadataRaw, err := json.Marshal(tracing.MetadataFromContext(b.ctx))
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{http.StatusInternalServerError, "marshal metadata", err}
|
||||
return nil, nil, nil, BuildError{http.StatusInternalServerError, "marshal metadata", err}
|
||||
}
|
||||
|
||||
tags, err := b.getProvisionerTags()
|
||||
if err != nil {
|
||||
return nil, nil, err // already wrapped BuildError
|
||||
return nil, nil, nil, err // already wrapped BuildError
|
||||
}
|
||||
|
||||
now := dbtime.Now()
|
||||
@@ -329,20 +331,35 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{http.StatusInternalServerError, "insert provisioner job", err}
|
||||
return nil, nil, nil, BuildError{http.StatusInternalServerError, "insert provisioner job", err}
|
||||
}
|
||||
|
||||
// nolint:gocritic // The user performing this request may not have permission
|
||||
// to read all provisioner daemons. We need to retrieve the eligible
|
||||
// provisioner daemons for this job to show in the UI if there is no
|
||||
// matching provisioner daemon.
|
||||
provisionerDaemons, err := b.store.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: template.OrganizationID,
|
||||
WantTags: provisionerJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
// NOTE: we do **not** want to fail a workspace build if we fail to
|
||||
// retrieve provisioner daemons. This is just to show in the UI if there
|
||||
// is no matching provisioner daemon for the job.
|
||||
provisionerDaemons = []database.ProvisionerDaemon{}
|
||||
}
|
||||
|
||||
templateVersionID, err := b.getTemplateVersionID()
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{http.StatusInternalServerError, "compute template version ID", err}
|
||||
return nil, nil, nil, BuildError{http.StatusInternalServerError, "compute template version ID", err}
|
||||
}
|
||||
buildNum, err := b.getBuildNumber()
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{http.StatusInternalServerError, "compute build number", err}
|
||||
return nil, nil, nil, BuildError{http.StatusInternalServerError, "compute build number", err}
|
||||
}
|
||||
state, err := b.getState()
|
||||
if err != nil {
|
||||
return nil, nil, BuildError{http.StatusInternalServerError, "compute build state", err}
|
||||
return nil, nil, nil, BuildError{http.StatusInternalServerError, "compute build state", err}
|
||||
}
|
||||
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
@@ -393,10 +410,10 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return &workspaceBuild, &provisionerJob, nil
|
||||
return &workspaceBuild, &provisionerJob, provisionerDaemons, nil
|
||||
}
|
||||
|
||||
func (b *Builder) getTemplate() (*database.Template, error) {
|
||||
|
||||
@@ -61,6 +61,7 @@ func TestBuilder_NoOptions(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -94,7 +95,8 @@ func TestBuilder_NoOptions(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
@@ -114,6 +116,7 @@ func TestBuilder_Initiator(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -130,7 +133,8 @@ func TestBuilder_Initiator(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
@@ -157,6 +161,7 @@ func TestBuilder_Baggage(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -172,7 +177,8 @@ func TestBuilder_Baggage(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
@@ -192,6 +198,7 @@ func TestBuilder_Reason(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(_ database.InsertProvisionerJobParams) {
|
||||
@@ -207,7 +214,8 @@ func TestBuilder_Reason(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Reason(database.BuildReasonAutostart)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
@@ -226,6 +234,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
|
||||
withLastBuildNotFound,
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
// previous rich parameters are not queried because there is no previous build.
|
||||
|
||||
// Outputs
|
||||
@@ -247,7 +256,8 @@ func TestBuilder_ActiveVersion(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).ActiveVersion()
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
@@ -314,6 +324,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -343,7 +354,8 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(buildParameters)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
@@ -404,6 +416,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -422,7 +435,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
})
|
||||
t.Run("UsePreviousParameterValues", func(t *testing.T) {
|
||||
@@ -448,6 +462,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -466,7 +481,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
})
|
||||
|
||||
@@ -502,7 +518,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
bldErr := wsbuilder.BuildError{}
|
||||
req.ErrorAs(err, &bldErr)
|
||||
asrt.Equal(http.StatusBadRequest, bldErr.Status)
|
||||
@@ -536,7 +552,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
|
||||
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
bldErr := wsbuilder.BuildError{}
|
||||
req.ErrorAs(err, &bldErr)
|
||||
asrt.Equal(http.StatusBadRequest, bldErr.Status)
|
||||
@@ -579,6 +596,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -599,7 +617,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
|
||||
RichParameterValues(nextBuildParameters).
|
||||
VersionID(activeVersionID)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
})
|
||||
|
||||
@@ -640,6 +658,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -660,7 +679,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
|
||||
RichParameterValues(nextBuildParameters).
|
||||
VersionID(activeVersionID)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
})
|
||||
|
||||
@@ -699,6 +718,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -719,7 +739,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
|
||||
RichParameterValues(nextBuildParameters).
|
||||
VersionID(activeVersionID)
|
||||
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
// nolint: dogsled
|
||||
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
|
||||
req.NoError(err)
|
||||
})
|
||||
}
|
||||
@@ -987,3 +1008,9 @@ func expectBuildParameters(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func withProvisionerDaemons(provisionerDaemons []database.ProvisionerDaemon) func(mTx *dbmock.MockStore) {
|
||||
return func(mTx *dbmock.MockStore) {
|
||||
mTx.EXPECT().GetProvisionerDaemonsByOrganization(gomock.Any(), gomock.Any()).Return(provisionerDaemons, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,22 @@ func (c *Client) TemplateVersionDryRun(ctx context.Context, version, job uuid.UU
|
||||
return j, json.NewDecoder(res.Body).Decode(&j)
|
||||
}
|
||||
|
||||
// TemplateVersionDryRunMatchedProvisioners returns the matched provisioners for a
|
||||
// template version dry-run job.
|
||||
func (c *Client) TemplateVersionDryRunMatchedProvisioners(ctx context.Context, version, job uuid.UUID) (MatchedProvisioners, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/dry-run/%s/matched-provisioners", version, job), nil)
|
||||
if err != nil {
|
||||
return MatchedProvisioners{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return MatchedProvisioners{}, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var matched MatchedProvisioners
|
||||
return matched, json.NewDecoder(res.Body).Decode(&matched)
|
||||
}
|
||||
|
||||
// TemplateVersionDryRunResources returns the resources of a finished template
|
||||
// version dry-run job.
|
||||
func (c *Client) TemplateVersionDryRunResources(ctx context.Context, version, job uuid.UUID) ([]WorkspaceResource, error) {
|
||||
|
||||
+22
-21
@@ -51,27 +51,28 @@ const (
|
||||
// WorkspaceBuild is an at-point representation of a workspace state.
|
||||
// BuildNumbers start at 1 and increase by 1 for each subsequent build
|
||||
type WorkspaceBuild struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
|
||||
WorkspaceName string `json:"workspace_name"`
|
||||
WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"`
|
||||
WorkspaceOwnerName string `json:"workspace_owner_name"`
|
||||
WorkspaceOwnerAvatarURL string `json:"workspace_owner_avatar_url"`
|
||||
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
|
||||
TemplateVersionName string `json:"template_version_name"`
|
||||
BuildNumber int32 `json:"build_number"`
|
||||
Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"`
|
||||
InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"`
|
||||
InitiatorUsername string `json:"initiator_name"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
|
||||
Resources []WorkspaceResource `json:"resources"`
|
||||
Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
|
||||
MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"`
|
||||
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
|
||||
DailyCost int32 `json:"daily_cost"`
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
|
||||
WorkspaceName string `json:"workspace_name"`
|
||||
WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"`
|
||||
WorkspaceOwnerName string `json:"workspace_owner_name"`
|
||||
WorkspaceOwnerAvatarURL string `json:"workspace_owner_avatar_url"`
|
||||
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
|
||||
TemplateVersionName string `json:"template_version_name"`
|
||||
BuildNumber int32 `json:"build_number"`
|
||||
Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"`
|
||||
InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"`
|
||||
InitiatorUsername string `json:"initiator_name"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
|
||||
Resources []WorkspaceResource `json:"resources"`
|
||||
Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
|
||||
MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"`
|
||||
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
|
||||
DailyCost int32 `json:"daily_cost"`
|
||||
MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"`
|
||||
}
|
||||
|
||||
// WorkspaceResource describes resources used to create a workspace, for instance:
|
||||
|
||||
Generated
+29
@@ -52,6 +52,11 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -237,6 +242,11 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -856,6 +866,11 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -1114,6 +1129,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -1277,6 +1297,10 @@ Status Code **200**
|
||||
| `»» tags` | object | false | | |
|
||||
| `»»» [any property]` | string | false | | |
|
||||
| `»» worker_id` | string(uuid) | false | | |
|
||||
| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | |
|
||||
| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. |
|
||||
| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. |
|
||||
| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. |
|
||||
| `» max_deadline` | string(date-time) | false | | |
|
||||
| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | |
|
||||
| `» resources` | array | false | | |
|
||||
@@ -1500,6 +1524,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
|
||||
Generated
+16
@@ -6602,6 +6602,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -7300,6 +7305,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -7439,6 +7449,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
| `initiator_id` | string | false | | |
|
||||
| `initiator_name` | string | false | | |
|
||||
| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | |
|
||||
| `matched_provisioners` | [codersdk.MatchedProvisioners](#codersdkmatchedprovisioners) | false | | |
|
||||
| `max_deadline` | string | false | | |
|
||||
| `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | |
|
||||
| `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | |
|
||||
@@ -7926,6 +7937,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
|
||||
Generated
+40
@@ -1944,6 +1944,46 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get template version dry-run matched provisioners
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----------------- | ---- | ------------ | -------- | ------------------- |
|
||||
| `templateversion` | path | string(uuid) | true | Template version ID |
|
||||
| `jobID` | path | string(uuid) | true | Job ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get template version dry-run resources by job ID
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+30
@@ -91,6 +91,11 @@ of the template will be used.
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -309,6 +314,11 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -551,6 +561,11 @@ of the template will be used.
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -772,6 +787,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -987,6 +1007,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
@@ -1321,6 +1346,11 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
|
||||
},
|
||||
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
|
||||
},
|
||||
"matched_provisioners": {
|
||||
"available": 0,
|
||||
"count": 0,
|
||||
"most_recently_seen": "2019-08-24T14:15:22Z"
|
||||
},
|
||||
"max_deadline": "2019-08-24T14:15:22Z",
|
||||
"reason": "initiator",
|
||||
"resources": [
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestWorkspaceBuild(t *testing.T) {
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
_, err = c.Client.CreateWorkspace(ctx, owner.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
_, err = c.Client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
TemplateVersionID: oldVersion.ID,
|
||||
Name: "abc123",
|
||||
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
|
||||
|
||||
Generated
+1
@@ -2012,6 +2012,7 @@ export interface WorkspaceBuild {
|
||||
readonly max_deadline?: string;
|
||||
readonly status: WorkspaceStatus;
|
||||
readonly daily_cost: number;
|
||||
readonly matched_provisioners?: MatchedProvisioners;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
|
||||
Reference in New Issue
Block a user