Skip to content

Commit 4578e6b

Browse files
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 6da1760 commit 4578e6b

30 files changed

+1058
-166
lines changed

cli/cliutil/provisionerwarn.go

Lines changed: 53 additions & 0 deletions
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

Lines changed: 74 additions & 0 deletions
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

Lines changed: 10 additions & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 44 additions & 0 deletions
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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 2 additions & 37 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy