Skip to content

Commit 181543c

Browse files
committed
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> (cherry picked from commit 2b57dcc)
1 parent 6f6787d commit 181543c

30 files changed

+1058
-166
lines changed

cli/cliutil/provisionerwarn.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cliutil
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"strings"
8+
9+
"github.com/coder/coder/v2/cli/cliui"
10+
"github.com/coder/coder/v2/codersdk"
11+
)
12+
13+
var (
14+
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.
15+
Details:
16+
Provisioner job ID : %s
17+
Requested tags : %s
18+
`
19+
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.
20+
Details:
21+
Provisioner job ID : %s
22+
Requested tags : %s
23+
Most recently seen : %s
24+
`
25+
)
26+
27+
// WarnMatchedProvisioners warns the user if there are no provisioners that
28+
// match the requested tags for a given provisioner job.
29+
// If the job is not pending, it is ignored.
30+
func WarnMatchedProvisioners(w io.Writer, mp *codersdk.MatchedProvisioners, job codersdk.ProvisionerJob) {
31+
if mp == nil {
32+
// Nothing in the response, nothing to do here!
33+
return
34+
}
35+
if job.Status != codersdk.ProvisionerJobPending {
36+
// Only warn if the job is pending.
37+
return
38+
}
39+
var tagsJSON strings.Builder
40+
if err := json.NewEncoder(&tagsJSON).Encode(job.Tags); err != nil {
41+
// Fall back to the less-pretty string representation.
42+
tagsJSON.Reset()
43+
_, _ = tagsJSON.WriteString(fmt.Sprintf("%v", job.Tags))
44+
}
45+
if mp.Count == 0 {
46+
cliui.Warnf(w, warnNoMatchedProvisioners, job.ID, tagsJSON.String())
47+
return
48+
}
49+
if mp.Available == 0 {
50+
cliui.Warnf(w, warnNoAvailableProvisioners, job.ID, strings.TrimSpace(tagsJSON.String()), mp.MostRecentlySeen.Time)
51+
return
52+
}
53+
}

cli/cliutil/provisionerwarn_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package cliutil_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/cli/cliutil"
10+
"github.com/coder/coder/v2/codersdk"
11+
)
12+
13+
func TestWarnMatchedProvisioners(t *testing.T) {
14+
t.Parallel()
15+
16+
for _, tt := range []struct {
17+
name string
18+
mp *codersdk.MatchedProvisioners
19+
job codersdk.ProvisionerJob
20+
expect string
21+
}{
22+
{
23+
name: "no_match",
24+
mp: &codersdk.MatchedProvisioners{
25+
Count: 0,
26+
Available: 0,
27+
},
28+
job: codersdk.ProvisionerJob{
29+
Status: codersdk.ProvisionerJobPending,
30+
},
31+
expect: `there are no provisioners that accept the required tags`,
32+
},
33+
{
34+
name: "no_available",
35+
mp: &codersdk.MatchedProvisioners{
36+
Count: 1,
37+
Available: 0,
38+
},
39+
job: codersdk.ProvisionerJob{
40+
Status: codersdk.ProvisionerJobPending,
41+
},
42+
expect: `Provisioners that accept the required tags have not responded for longer than expected`,
43+
},
44+
{
45+
name: "match",
46+
mp: &codersdk.MatchedProvisioners{
47+
Count: 1,
48+
Available: 1,
49+
},
50+
job: codersdk.ProvisionerJob{
51+
Status: codersdk.ProvisionerJobPending,
52+
},
53+
},
54+
{
55+
name: "not_pending",
56+
mp: &codersdk.MatchedProvisioners{},
57+
job: codersdk.ProvisionerJob{
58+
Status: codersdk.ProvisionerJobRunning,
59+
},
60+
},
61+
} {
62+
tt := tt
63+
t.Run(tt.name, func(t *testing.T) {
64+
t.Parallel()
65+
var w strings.Builder
66+
cliutil.WarnMatchedProvisioners(&w, tt.mp, tt.job)
67+
if tt.expect != "" {
68+
require.Contains(t, w.String(), tt.expect)
69+
} else {
70+
require.Empty(t, w.String())
71+
}
72+
})
73+
}
74+
}

cli/create.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/coder/pretty"
1515

1616
"github.com/coder/coder/v2/cli/cliui"
17+
"github.com/coder/coder/v2/cli/cliutil"
1718
"github.com/coder/coder/v2/coderd/util/ptr"
1819
"github.com/coder/coder/v2/coderd/util/slice"
1920
"github.com/coder/coder/v2/codersdk"
@@ -289,7 +290,7 @@ func (r *RootCmd) create() *serpent.Command {
289290
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
290291
}
291292

292-
workspace, err := client.CreateWorkspace(inv.Context(), template.OrganizationID, workspaceOwner, codersdk.CreateWorkspaceRequest{
293+
workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, codersdk.CreateWorkspaceRequest{
293294
TemplateVersionID: templateVersionID,
294295
Name: workspaceName,
295296
AutostartSchedule: schedSpec,
@@ -301,6 +302,8 @@ func (r *RootCmd) create() *serpent.Command {
301302
return xerrors.Errorf("create workspace: %w", err)
302303
}
303304

305+
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
306+
304307
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, workspace.LatestBuild.ID)
305308
if err != nil {
306309
return xerrors.Errorf("watch build: %w", err)
@@ -433,6 +436,12 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
433436
if err != nil {
434437
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
435438
}
439+
440+
matchedProvisioners, err := client.TemplateVersionDryRunMatchedProvisioners(inv.Context(), templateVersion.ID, dryRun.ID)
441+
if err != nil {
442+
return nil, xerrors.Errorf("get matched provisioners: %w", err)
443+
}
444+
cliutil.WarnMatchedProvisioners(inv.Stdout, &matchedProvisioners, dryRun)
436445
_, _ = fmt.Fprintln(inv.Stdout, "Planning workspace...")
437446
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
438447
Fetch: func() (codersdk.ProvisionerJob, error) {

cli/delete.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/coder/coder/v2/cli/cliui"
8+
"github.com/coder/coder/v2/cli/cliutil"
89
"github.com/coder/coder/v2/codersdk"
910
"github.com/coder/serpent"
1011
)
@@ -55,6 +56,7 @@ func (r *RootCmd) deleteWorkspace() *serpent.Command {
5556
if err != nil {
5657
return err
5758
}
59+
cliutil.WarnMatchedProvisioners(inv.Stdout, build.MatchedProvisioners, build.Job)
5860

5961
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
6062
if err != nil {

cli/delete_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/coder/coder/v2/cli/clitest"
1313
"github.com/coder/coder/v2/coderd/coderdtest"
1414
"github.com/coder/coder/v2/coderd/database/dbauthz"
15+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
16+
"github.com/coder/coder/v2/coderd/rbac"
1517
"github.com/coder/coder/v2/codersdk"
1618
"github.com/coder/coder/v2/pty/ptytest"
1719
"github.com/coder/coder/v2/testutil"
@@ -164,4 +166,46 @@ func TestDelete(t *testing.T) {
164166
}()
165167
<-doneChan
166168
})
169+
170+
t.Run("WarnNoProvisioners", func(t *testing.T) {
171+
t.Parallel()
172+
if !dbtestutil.WillUsePostgres() {
173+
t.Skip("this test requires postgres")
174+
}
175+
176+
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
177+
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
178+
Database: store,
179+
Pubsub: ps,
180+
IncludeProvisionerDaemon: true,
181+
})
182+
183+
// Given: a user, template, and workspace
184+
user := coderdtest.CreateFirstUser(t, client)
185+
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
186+
version := coderdtest.CreateTemplateVersion(t, templateAdmin, user.OrganizationID, nil)
187+
template := coderdtest.CreateTemplate(t, templateAdmin, user.OrganizationID, version.ID)
188+
workspace := coderdtest.CreateWorkspace(t, templateAdmin, template.ID)
189+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, workspace.LatestBuild.ID)
190+
191+
// When: all provisioner daemons disappear
192+
require.NoError(t, closeDaemon.Close())
193+
_, err := db.Exec("DELETE FROM provisioner_daemons;")
194+
require.NoError(t, err)
195+
196+
// Then: the workspace deletion should warn about no provisioners
197+
inv, root := clitest.New(t, "delete", workspace.Name, "-y")
198+
pty := ptytest.New(t).Attach(inv)
199+
clitest.SetupConfig(t, templateAdmin, root)
200+
doneChan := make(chan struct{})
201+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
202+
defer cancel()
203+
go func() {
204+
defer close(doneChan)
205+
_ = inv.WithContext(ctx).Run()
206+
}()
207+
pty.ExpectMatch("there are no provisioners that accept the required tags")
208+
cancel()
209+
<-doneChan
210+
})
167211
}

cli/start.go

+19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"golang.org/x/xerrors"
99

1010
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/cli/cliutil"
1112
"github.com/coder/coder/v2/codersdk"
1213
"github.com/coder/serpent"
1314
)
@@ -35,6 +36,23 @@ func (r *RootCmd) start() *serpent.Command {
3536
}
3637
var build codersdk.WorkspaceBuild
3738
switch workspace.LatestBuild.Status {
39+
case codersdk.WorkspaceStatusPending:
40+
// The above check is technically duplicated in cliutil.WarnmatchedProvisioners
41+
// but we still want to avoid users spamming multiple builds that will
42+
// not be picked up.
43+
_, _ = fmt.Fprintf(
44+
inv.Stdout,
45+
"\nThe %s workspace is waiting to start!\n",
46+
cliui.Keyword(workspace.Name),
47+
)
48+
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
49+
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
50+
Text: "Enqueue another start?",
51+
IsConfirm: true,
52+
Default: cliui.ConfirmNo,
53+
}); err != nil {
54+
return err
55+
}
3856
case codersdk.WorkspaceStatusRunning:
3957
_, _ = fmt.Fprintf(
4058
inv.Stdout, "\nThe %s workspace is already running!\n",
@@ -159,6 +177,7 @@ func startWorkspace(inv *serpent.Invocation, client *codersdk.Client, workspace
159177
if err != nil {
160178
return codersdk.WorkspaceBuild{}, xerrors.Errorf("create workspace build: %w", err)
161179
}
180+
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
162181

163182
return build, nil
164183
}

cli/stop.go

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/coder/coder/v2/cli/cliui"
8+
"github.com/coder/coder/v2/cli/cliutil"
89
"github.com/coder/coder/v2/codersdk"
910
"github.com/coder/serpent"
1011
)
@@ -36,6 +37,21 @@ func (r *RootCmd) stop() *serpent.Command {
3637
if err != nil {
3738
return err
3839
}
40+
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobPending {
41+
// cliutil.WarnMatchedProvisioners also checks if the job is pending
42+
// but we still want to avoid users spamming multiple builds that will
43+
// not be picked up.
44+
cliui.Warn(inv.Stderr, "The workspace is already stopping!")
45+
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
46+
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
47+
Text: "Enqueue another stop?",
48+
IsConfirm: true,
49+
Default: cliui.ConfirmNo,
50+
}); err != nil {
51+
return err
52+
}
53+
}
54+
3955
wbr := codersdk.CreateWorkspaceBuildRequest{
4056
Transition: codersdk.WorkspaceTransitionStop,
4157
}
@@ -46,6 +62,7 @@ func (r *RootCmd) stop() *serpent.Command {
4662
if err != nil {
4763
return err
4864
}
65+
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
4966

5067
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
5168
if err != nil {

cli/templatepush.go

+2-37
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cli
22

33
import (
44
"bufio"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"io"
@@ -17,6 +16,7 @@ import (
1716
"golang.org/x/xerrors"
1817

1918
"github.com/coder/coder/v2/cli/cliui"
19+
"github.com/coder/coder/v2/cli/cliutil"
2020
"github.com/coder/coder/v2/codersdk"
2121
"github.com/coder/coder/v2/provisionersdk"
2222
"github.com/coder/pretty"
@@ -416,7 +416,7 @@ func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplat
416416
if err != nil {
417417
return nil, err
418418
}
419-
WarnMatchedProvisioners(inv, version)
419+
cliutil.WarnMatchedProvisioners(inv.Stderr, version.MatchedProvisioners, version.Job)
420420
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
421421
Fetch: func() (codersdk.ProvisionerJob, error) {
422422
version, err := client.TemplateVersion(inv.Context(), version.ID)
@@ -482,41 +482,6 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
482482
return tags, nil
483483
}
484484

485-
var (
486-
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.
487-
Details:
488-
Provisioner job ID : %s
489-
Requested tags : %s
490-
`
491-
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.
492-
Details:
493-
Provisioner job ID : %s
494-
Requested tags : %s
495-
Most recently seen : %s
496-
`
497-
)
498-
499-
func WarnMatchedProvisioners(inv *serpent.Invocation, tv codersdk.TemplateVersion) {
500-
if tv.MatchedProvisioners == nil {
501-
// Nothing in the response, nothing to do here!
502-
return
503-
}
504-
var tagsJSON strings.Builder
505-
if err := json.NewEncoder(&tagsJSON).Encode(tv.Job.Tags); err != nil {
506-
// Fall back to the less-pretty string representation.
507-
tagsJSON.Reset()
508-
_, _ = tagsJSON.WriteString(fmt.Sprintf("%v", tv.Job.Tags))
509-
}
510-
if tv.MatchedProvisioners.Count == 0 {
511-
cliui.Warnf(inv.Stderr, warnNoMatchedProvisioners, tv.Job.ID, tagsJSON.String())
512-
return
513-
}
514-
if tv.MatchedProvisioners.Available == 0 {
515-
cliui.Warnf(inv.Stderr, warnNoAvailableProvisioners, tv.Job.ID, strings.TrimSpace(tagsJSON.String()), tv.MatchedProvisioners.MostRecentlySeen.Time)
516-
return
517-
}
518-
}
519-
520485
// prettyDirectoryPath returns a prettified path when inside the users
521486
// home directory. Falls back to dir if the users home directory cannot
522487
// discerned. This function calls filepath.Clean on the result.

0 commit comments

Comments
 (0)