Skip to content

feat!: extract provisioner tags from coder_workspace_tags data source #15578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add codersdk.MatchedProvisioners
  • Loading branch information
johnstcn committed Nov 22, 2024
commit 80dc6c211b3929b5222f50ab10342b7601e87642
21 changes: 21 additions & 0 deletions coderd/apidoc/docs.go

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

21 changes: 21 additions & 0 deletions coderd/apidoc/swagger.json

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

59 changes: 35 additions & 24 deletions coderd/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]), warnings))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, warnings))
}

// @Summary Patch template version by ID
Expand Down Expand Up @@ -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]), nil))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
}

// @Summary Cancel template version by ID
Expand Down Expand Up @@ -814,7 +814,7 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
return err
}

apiVersions = append(apiVersions, convertTemplateVersion(version, convertProvisionerJob(job), nil))
apiVersions = append(apiVersions, convertTemplateVersion(version, convertProvisionerJob(job), codersdk.MatchedProvisioners{}, nil))
}

return nil
Expand Down Expand Up @@ -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]), nil))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
}

// @Summary Get template version by organization, template, and name
Expand Down Expand Up @@ -934,7 +934,7 @@ func (api *API) templateVersionByOrganizationTemplateAndName(rw http.ResponseWri
return
}

httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), nil))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
}

// @Summary Get previous template version by organization, template, and name
Expand Down Expand Up @@ -1020,7 +1020,7 @@ func (api *API) previousTemplateVersionByOrganizationTemplateAndName(rw http.Res
return
}

httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), nil))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
}

// @Summary Archive template unused versions by template id
Expand Down Expand Up @@ -1488,6 +1488,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
var templateVersion database.TemplateVersion
var provisionerJob database.ProvisionerJob
var warnings []codersdk.TemplateVersionWarning
var matchedProvisioners codersdk.MatchedProvisioners
err = api.Database.InTx(func(tx database.Store) error {
jobID := uuid.New()

Expand All @@ -1514,17 +1515,17 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht

// Check for eligible provisioners. This allows us to log a message warning deployment administrators
// of users submitting jobs for which no provisioners are available.
allProvisioners, activeProvisioners, err := checkProvisioners(ctx, tx, organization.ID, tags, api.DeploymentValues.Provisioner.DaemonPollInterval.Value())
matchedProvisioners, err = checkProvisioners(ctx, tx, organization.ID, tags, api.DeploymentValues.Provisioner.DaemonPollInterval.Value())
if err != nil {
api.Logger.Error(ctx, "failed to check eligible provisioner daemons for job", slog.Error(err))
} else if activeProvisioners == 0 {
} 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 allProvisioners == 0 {
} 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),
Expand Down Expand Up @@ -1622,10 +1623,14 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
}

httpapi.Write(ctx, rw, http.StatusCreated, convertTemplateVersion(templateVersion, convertProvisionerJob(database.GetProvisionerJobsByIDsWithQueuePositionRow{
ProvisionerJob: provisionerJob,
QueuePosition: 0,
}), warnings))
httpapi.Write(ctx, rw, http.StatusCreated, convertTemplateVersion(
templateVersion,
convertProvisionerJob(database.GetProvisionerJobsByIDsWithQueuePositionRow{
ProvisionerJob: provisionerJob,
QueuePosition: 0,
}),
matchedProvisioners,
warnings))
}

// templateVersionResources returns the workspace agent resources associated
Expand Down Expand Up @@ -1692,7 +1697,7 @@ func (api *API) templateVersionLogs(rw http.ResponseWriter, r *http.Request) {
api.provisionerJobLogs(rw, r, job)
}

func convertTemplateVersion(version database.TemplateVersion, job codersdk.ProvisionerJob, 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,
Expand All @@ -1708,8 +1713,9 @@ func convertTemplateVersion(version database.TemplateVersion, job codersdk.Provi
Username: version.CreatedByUsername,
AvatarURL: version.CreatedByAvatarURL,
},
Archived: version.Archived,
Warnings: warnings,
Archived: version.Archived,
Warnings: warnings,
MatchedProvisioners: matchedProvisioners,
}
}

Expand Down Expand Up @@ -1813,7 +1819,7 @@ func (api *API) publishTemplateUpdate(ctx context.Context, templateID uuid.UUID)
}
}

func checkProvisioners(ctx context.Context, store database.Store, orgID uuid.UUID, wantTags map[string]string, pollInterval time.Duration) (found, active int, err error) {
func checkProvisioners(ctx context.Context, store database.Store, orgID uuid.UUID, wantTags map[string]string, pollInterval time.Duration) (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{
Expand All @@ -1822,19 +1828,24 @@ func checkProvisioners(ctx context.Context, store database.Store, orgID uuid.UUI
})
if err != nil {
// Log the error but do not return any warnings. This is purely advisory and we should not block.
return -1, -1, xerrors.Errorf("get provisioner daemons by organization: %w", err)
return codersdk.MatchedProvisioners{}, xerrors.Errorf("provisioner daemons by organization: %w", err)
}

onePollAgo := time.Now().Add(-pollInterval)
threePollsAgo := time.Now().Add(-3 * pollInterval)
Copy link
Member Author

Choose a reason for hiding this comment

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

review: this is duplicated from / in line with coderd/healthcheck:

	if opts.StaleInterval == 0 {
		opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3
	}

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need the above as a code comment or perhaps a test that checks that they remain in sync?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure of the value of adding a test for this, but we can possibly refactor where it's defined in a follow-up so that it's consistent. For now, I'll add a comment 👍

Copy link
Member

@mafredri mafredri Nov 25, 2024

Choose a reason for hiding this comment

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

On the surface it kinda makes sense to use the same logic (3*poll), but at the same time I feel multiplying by 3 is pretty extreme, I'm more in favor of a fixed time like poll+30s or something like that. In this instance poll+30s would surface the issue in half the time. If poll is ever raised, to say 2min, the multiplication would go from 1.5min to 3.5min longer to surface the issue, etc.

mostRecentlySeen := codersdk.NullTime{}
var matched codersdk.MatchedProvisioners
for _, provisioner := range eligibleProvisioners {
if !provisioner.LastSeenAt.Valid {
continue
}
found++
if provisioner.LastSeenAt.Time.Before(onePollAgo) {
continue
matched.Count++
if provisioner.LastSeenAt.Time.After(threePollsAgo) {
matched.Available++
}
if provisioner.LastSeenAt.Time.After(mostRecentlySeen.Time) {
matched.MostRecentlySeen.Valid = true
matched.MostRecentlySeen.Time = provisioner.LastSeenAt.Time
}
active++
}
return found, active, nil
return matched, nil
}
5 changes: 5 additions & 0 deletions coderd/templateversions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
} 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)
})
}
})
Expand Down
16 changes: 16 additions & 0 deletions codersdk/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ type ProvisionerDaemon struct {
Tags map[string]string `json:"tags"`
}

// MatchedProvisioners represents the number of provisioner daemons
// available to take a job at a specific point in time.
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
// requested tags.
Count int `json:"count"`
// 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.
Available int `json:"available"`
// MostRecentlySeen is the most recently seen time of the set of matched
// provisioners. If no provisioners matched, this field will be null.
MostRecentlySeen NullTime `json:"most_recently_seen,omitempty" format:"date-time"`
}
Copy link
Member

Choose a reason for hiding this comment

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

❤️


// ProvisionerJobStatus represents the at-time state of a job.
type ProvisionerJobStatus string

Expand Down
3 changes: 2 additions & 1 deletion codersdk/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type TemplateVersion struct {
CreatedBy MinimalUser `json:"created_by"`
Archived bool `json:"archived"`

Warnings []TemplateVersionWarning `json:"warnings,omitempty" enums:"DEPRECATED_PARAMETERS"`
Warnings []TemplateVersionWarning `json:"warnings,omitempty" enums:"DEPRECATED_PARAMETERS"`
MatchedProvisioners MatchedProvisioners `json:"matched_provisioners,omitempty"`
Copy link
Member Author

@johnstcn johnstcn Nov 22, 2024

Choose a reason for hiding this comment

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

review: I considered adding as a field on the ProvisionerJob but it did not seem 100% correct.

However, as it does not make sense to return this from e.g. getting a previous template version, it may make sense instead to make this nullable (e.g. *MatchedProvisioners). Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

From what I could see last week, the only way to see tags for a workspace build is to get that workspace's template version. Therefore, I think we need this even for previous template versions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looked at where else we use this payload. You're right, it isn't necessary elsewhere. That means we could make it nullable. Whether that would be better for API clients, I'm not sure. Would be happy if you made it nullable. Would be happy if you left it as is.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll keep it as-is for now so you can build on top of it. It should be a fairly simple refactor to make it a nullable field later.

}

type TemplateVersionExternalAuth struct {
Expand Down
52 changes: 38 additions & 14 deletions docs/reference/api/schemas.md

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

Loading
Loading