diff --git a/cli/templatepush.go b/cli/templatepush.go index 8516d7f9c1310..2576e878edddb 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -416,30 +416,7 @@ func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplat if err != nil { return nil, err } - var tagsJSON strings.Builder - if err := json.NewEncoder(&tagsJSON).Encode(version.Job.Tags); err != nil { - // Fall back to the less-pretty string representation. - tagsJSON.Reset() - _, _ = tagsJSON.WriteString(fmt.Sprintf("%v", version.Job.Tags)) - } - if version.MatchedProvisioners.Count == 0 { - cliui.Warnf(inv.Stderr, `No provisioners are available to handle the job! -Please contact your deployment administrator for assistance. -Details: - Provisioner job ID : %s - Requested tags : %s -`, version.Job.ID, tagsJSON.String()) - } else if version.MatchedProvisioners.Available == 0 { - cliui.Warnf(inv.Stderr, `All available provisioner daemons have been silent for a while. -Your build will proceed once they become available. -If this persists, please contact your deployment administrator for assistance. -Details: - Provisioner job ID : %s - Requested tags : %s - Most recently seen : %s -`, version.Job.ID, strings.TrimSpace(tagsJSON.String()), version.MatchedProvisioners.MostRecentlySeen.Time) - } - + WarnMatchedProvisioners(inv, version) err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{ Fetch: func() (codersdk.ProvisionerJob, error) { version, err := client.TemplateVersion(inv.Context(), version.ID) @@ -505,6 +482,41 @@ 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. diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index a20e3070740a8..ae8f60bd9c551 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -3,6 +3,7 @@ package cli_test import ( "bytes" "context" + "database/sql" "os" "path/filepath" "runtime" @@ -18,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisioner/echo" @@ -412,84 +414,162 @@ func TestTemplatePush(t *testing.T) { t.Run("WorkspaceTagsTerraform", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - // Start an instance **without** a built-in provisioner. - // We're not actually testing that the Terraform applies. - // What we test is that a provisioner job is created with the expected - // tags based on the __content__ of the Terraform. - store, ps := dbtestutil.NewDB(t) - client := coderdtest.New(t, &coderdtest.Options{ - Database: store, - Pubsub: ps, - }) - - owner := coderdtest.CreateFirstUser(t, client) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) - - // Create a tar file with some pre-defined content - tarFile := testutil.CreateTar(t, map[string]string{ - "main.tf": ` -variable "a" { - type = string - default = "1" -} -data "coder_parameter" "b" { - type = string - default = "2" -} -resource "null_resource" "test" {} -data "coder_workspace_tags" "tags" { - tags = { - "foo": "bar", - "a": var.a, - "b": data.coder_parameter.b.value, - } -}`, - }) - - // Write the tar file to disk. - tempDir := t.TempDir() - err := tfparse.WriteArchive(tarFile, "application/x-tar", tempDir) - require.NoError(t, err) - - // Run `coder templates push` - templateName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") - var stdout, stderr strings.Builder - inv, root := clitest.New(t, "templates", "push", templateName, "-d", tempDir, "--yes") - inv.Stdout = &stdout - inv.Stderr = &stderr - clitest.SetupConfig(t, templateAdmin, root) - - // Don't forget to clean up! - cancelCtx, cancel := context.WithCancel(ctx) - t.Cleanup(cancel) - done := make(chan error) - go func() { - done <- inv.WithContext(cancelCtx).Run() - }() - - // Assert that a provisioner job was created with the desired tags. - wantTags := database.StringMap(provisionersdk.MutateTags(uuid.Nil, map[string]string{ - "foo": "bar", - "a": "1", - "b": "2", - })) - require.Eventually(t, func() bool { - jobs, err := store.GetProvisionerJobsCreatedAfter(ctx, time.Time{}) - if !assert.NoError(t, err) { - return false - } - if len(jobs) == 0 { - return false - } - return assert.EqualValues(t, wantTags, jobs[0].Tags) - }, testutil.WaitShort, testutil.IntervalSlow) - - cancel() - <-done + tests := []struct { + name string + setupDaemon func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error + expectOutput string + }{ + { + name: "no provisioners available", + setupDaemon: func(_ context.Context, _ database.Store, _ codersdk.CreateFirstUserResponse, _ database.StringMap, _ time.Time) error { + return nil + }, + expectOutput: "there are no provisioners that accept the required tags", + }, + { + name: "provisioner stale", + setupDaemon: func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error { + pk, err := store.InsertProvisionerKey(ctx, database.InsertProvisionerKeyParams{ + ID: uuid.New(), + CreatedAt: now, + OrganizationID: owner.OrganizationID, + Name: "test", + Tags: tags, + HashedSecret: []byte("secret"), + }) + if err != nil { + return err + } + oneHourAgo := now.Add(-time.Hour) + _, err = store.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ + Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform}, + LastSeenAt: sql.NullTime{Time: oneHourAgo, Valid: true}, + CreatedAt: oneHourAgo, + Name: "test", + Tags: tags, + OrganizationID: owner.OrganizationID, + KeyID: pk.ID, + }) + return err + }, + expectOutput: "Provisioners that accept the required tags have not responded for longer than expected", + }, + { + name: "active provisioner", + setupDaemon: func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error { + pk, err := store.InsertProvisionerKey(ctx, database.InsertProvisionerKeyParams{ + ID: uuid.New(), + CreatedAt: now, + OrganizationID: owner.OrganizationID, + Name: "test", + Tags: tags, + HashedSecret: []byte("secret"), + }) + if err != nil { + return err + } + _, err = store.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ + Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform}, + LastSeenAt: sql.NullTime{Time: now, Valid: true}, + CreatedAt: now, + Name: "test-active", + Tags: tags, + OrganizationID: owner.OrganizationID, + KeyID: pk.ID, + }) + return err + }, + expectOutput: "", + }, + } - require.Contains(t, stderr.String(), "No provisioners are available to handle the job!") + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Start an instance **without** a built-in provisioner. + // We're not actually testing that the Terraform applies. + // What we test is that a provisioner job is created with the expected + // tags based on the __content__ of the Terraform. + store, ps := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: store, + Pubsub: ps, + }) + + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + // Create a tar file with some pre-defined content + tarFile := testutil.CreateTar(t, map[string]string{ + "main.tf": ` + variable "a" { + type = string + default = "1" + } + data "coder_parameter" "b" { + type = string + default = "2" + } + resource "null_resource" "test" {} + data "coder_workspace_tags" "tags" { + tags = { + "a": var.a, + "b": data.coder_parameter.b.value, + "test_name": "` + tt.name + `" + } + }`, + }) + + // Write the tar file to disk. + tempDir := t.TempDir() + err := tfparse.WriteArchive(tarFile, "application/x-tar", tempDir) + require.NoError(t, err) + + wantTags := database.StringMap(provisionersdk.MutateTags(uuid.Nil, map[string]string{ + "a": "1", + "b": "2", + "test_name": tt.name, + })) + + templateName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + + inv, root := clitest.New(t, "templates", "push", templateName, "-d", tempDir, "--yes") + clitest.SetupConfig(t, templateAdmin, root) + pty := ptytest.New(t).Attach(inv) + + ctx := testutil.Context(t, testutil.WaitShort) + now := dbtime.Now() + require.NoError(t, tt.setupDaemon(ctx, store, owner, wantTags, now)) + + cancelCtx, cancel := context.WithCancel(ctx) + t.Cleanup(cancel) + done := make(chan error) + go func() { + done <- inv.WithContext(cancelCtx).Run() + }() + + require.Eventually(t, func() bool { + jobs, err := store.GetProvisionerJobsCreatedAfter(ctx, time.Time{}) + if !assert.NoError(t, err) { + return false + } + if len(jobs) == 0 { + return false + } + return assert.EqualValues(t, wantTags, jobs[0].Tags) + }, testutil.WaitShort, testutil.IntervalFast) + + if tt.expectOutput != "" { + pty.ExpectMatch(tt.expectOutput) + } + + cancel() + <-done + }) + } }) t.Run("ChangeTags", func(t *testing.T) { diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 3e42879171a45..004dc6e5a4d6d 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -77,7 +77,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]), codersdk.MatchedProvisioners{}, warnings)) + httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil, warnings)) } // @Summary Patch template version by ID @@ -173,7 +173,7 @@ func (api *API) patchTemplateVersion(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil)) + httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), nil, nil)) } // @Summary Cancel template version by ID @@ -814,7 +814,7 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque return err } - apiVersions = append(apiVersions, convertTemplateVersion(version, convertProvisionerJob(job), codersdk.MatchedProvisioners{}, nil)) + apiVersions = append(apiVersions, convertTemplateVersion(version, convertProvisionerJob(job), nil, nil)) } return nil @@ -869,7 +869,7 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil)) + httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil, nil)) } // @Summary Get template version by organization, template, and name @@ -934,7 +934,7 @@ func (api *API) templateVersionByOrganizationTemplateAndName(rw http.ResponseWri return } - httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil)) + httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil, nil)) } // @Summary Get previous template version by organization, template, and name @@ -1020,7 +1020,7 @@ func (api *API) previousTemplateVersionByOrganizationTemplateAndName(rw http.Res return } - httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil)) + httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), nil, nil)) } // @Summary Archive template unused versions by template id @@ -1633,7 +1633,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht ProvisionerJob: provisionerJob, QueuePosition: 0, }), - matchedProvisioners, + &matchedProvisioners, warnings)) } @@ -1701,7 +1701,7 @@ func (api *API) templateVersionLogs(rw http.ResponseWriter, r *http.Request) { api.provisionerJobLogs(rw, r, job) } -func convertTemplateVersion(version database.TemplateVersion, job codersdk.ProvisionerJob, matchedProvisioners codersdk.MatchedProvisioners, warnings []codersdk.TemplateVersionWarning) codersdk.TemplateVersion { +func convertTemplateVersion(version database.TemplateVersion, job codersdk.ProvisionerJob, matchedProvisioners *codersdk.MatchedProvisioners, warnings []codersdk.TemplateVersionWarning) codersdk.TemplateVersion { return codersdk.TemplateVersion{ ID: version.ID, TemplateID: &version.TemplateID.UUID, diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 5e96de10d5058..2a13af0bc410a 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -471,14 +471,13 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID) require.NoError(t, err) require.EqualValues(t, tt.wantTags, pj.Tags) + // Also assert that we get the expected information back from the API endpoint + require.Zero(t, tv.MatchedProvisioners.Count) + require.Zero(t, tv.MatchedProvisioners.Available) + require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time) } else { require.ErrorContains(t, err, tt.expectError) } - - // Also assert that we get the expected information back from the API endpoint - require.Zero(t, tv.MatchedProvisioners.Count) - require.Zero(t, tv.MatchedProvisioners.Available) - require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time) }) } }) diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index acd0c6955ab7f..27d2766a7cd13 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -53,6 +53,7 @@ type ProvisionerDaemon struct { // MatchedProvisioners represents the number of provisioner daemons // available to take a job at a specific point in time. +// Introduced in Coder version 2.18.0. type MatchedProvisioners struct { // Count is the number of provisioner daemons that matched the given // tags. If the count is 0, it means no provisioner daemons matched the diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index 5bda52daf3dfe..4f07f84074ec4 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -32,7 +32,7 @@ type TemplateVersion struct { Archived bool `json:"archived"` Warnings []TemplateVersionWarning `json:"warnings,omitempty" enums:"DEPRECATED_PARAMETERS"` - MatchedProvisioners MatchedProvisioners `json:"matched_provisioners,omitempty"` + MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"` } type TemplateVersionExternalAuth struct { 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