mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
## Summary This PR introduces support for expiration policies in prebuilds. The TTL (time-to-live) is retrieved from the Terraform configuration ([terraform-provider-coder PR](https://github.com/coder/terraform-provider-coder/pull/404)): ``` prebuilds = { instances = 2 expiration_policy { ttl = 86400 } } ``` **Note**: Since there is no need for precise TTL enforcement down to the second, in this implementation expired prebuilds are handled in a single reconciliation cycle: they are deleted, and new instances are created only if needed to match the desired count. ## Changes * The outcome of a reconciliation cycle is now expressed as a slice of reconciliation actions, instead of a single aggregated action. * Adjusted reconciliation logic to delete expired prebuilds and guarantee that the number of desired instances is correct. * Updated relevant data structures and methods to support expiration policies parameters. * Added documentation to `Prebuilt workspaces` page * Update `terraform-provider-coder` to version 2.5.0: https://github.com/coder/terraform-provider-coder/releases/tag/v2.5.0 Depends on: https://github.com/coder/terraform-provider-coder/pull/404 Fixes: https://github.com/coder/coder/issues/17916
105 lines
3.4 KiB
Go
105 lines
3.4 KiB
Go
package prebuilds
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
)
|
|
|
|
// GlobalSnapshot represents a full point-in-time snapshot of state relating to prebuilds across all templates.
|
|
type GlobalSnapshot struct {
|
|
Presets []database.GetTemplatePresetsWithPrebuildsRow
|
|
RunningPrebuilds []database.GetRunningPrebuiltWorkspacesRow
|
|
PrebuildsInProgress []database.CountInProgressPrebuildsRow
|
|
Backoffs []database.GetPresetsBackoffRow
|
|
HardLimitedPresets []database.GetPresetsAtFailureLimitRow
|
|
}
|
|
|
|
func NewGlobalSnapshot(
|
|
presets []database.GetTemplatePresetsWithPrebuildsRow,
|
|
runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow,
|
|
prebuildsInProgress []database.CountInProgressPrebuildsRow,
|
|
backoffs []database.GetPresetsBackoffRow,
|
|
hardLimitedPresets []database.GetPresetsAtFailureLimitRow,
|
|
) GlobalSnapshot {
|
|
return GlobalSnapshot{
|
|
Presets: presets,
|
|
RunningPrebuilds: runningPrebuilds,
|
|
PrebuildsInProgress: prebuildsInProgress,
|
|
Backoffs: backoffs,
|
|
HardLimitedPresets: hardLimitedPresets,
|
|
}
|
|
}
|
|
|
|
func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, error) {
|
|
preset, found := slice.Find(s.Presets, func(preset database.GetTemplatePresetsWithPrebuildsRow) bool {
|
|
return preset.ID == presetID
|
|
})
|
|
if !found {
|
|
return nil, xerrors.Errorf("no preset found with ID %q", presetID)
|
|
}
|
|
|
|
// Only include workspaces that have successfully started
|
|
running := slice.Filter(s.RunningPrebuilds, func(prebuild database.GetRunningPrebuiltWorkspacesRow) bool {
|
|
if !prebuild.CurrentPresetID.Valid {
|
|
return false
|
|
}
|
|
return prebuild.CurrentPresetID.UUID == preset.ID
|
|
})
|
|
|
|
// Separate running workspaces into non-expired and expired based on the preset's TTL
|
|
nonExpired, expired := filterExpiredWorkspaces(preset, running)
|
|
|
|
inProgress := slice.Filter(s.PrebuildsInProgress, func(prebuild database.CountInProgressPrebuildsRow) bool {
|
|
return prebuild.PresetID.UUID == preset.ID
|
|
})
|
|
|
|
var backoffPtr *database.GetPresetsBackoffRow
|
|
backoff, found := slice.Find(s.Backoffs, func(row database.GetPresetsBackoffRow) bool {
|
|
return row.PresetID == preset.ID
|
|
})
|
|
if found {
|
|
backoffPtr = &backoff
|
|
}
|
|
|
|
_, isHardLimited := slice.Find(s.HardLimitedPresets, func(row database.GetPresetsAtFailureLimitRow) bool {
|
|
return row.PresetID == preset.ID
|
|
})
|
|
|
|
return &PresetSnapshot{
|
|
Preset: preset,
|
|
Running: nonExpired,
|
|
Expired: expired,
|
|
InProgress: inProgress,
|
|
Backoff: backoffPtr,
|
|
IsHardLimited: isHardLimited,
|
|
}, nil
|
|
}
|
|
|
|
// filterExpiredWorkspaces splits running workspaces into expired and non-expired
|
|
// based on the preset's TTL.
|
|
// If TTL is missing or zero, all workspaces are considered non-expired.
|
|
func filterExpiredWorkspaces(preset database.GetTemplatePresetsWithPrebuildsRow, runningWorkspaces []database.GetRunningPrebuiltWorkspacesRow) (nonExpired []database.GetRunningPrebuiltWorkspacesRow, expired []database.GetRunningPrebuiltWorkspacesRow) {
|
|
if !preset.Ttl.Valid {
|
|
return runningWorkspaces, expired
|
|
}
|
|
|
|
ttl := time.Duration(preset.Ttl.Int32) * time.Second
|
|
if ttl <= 0 {
|
|
return runningWorkspaces, expired
|
|
}
|
|
|
|
for _, prebuild := range runningWorkspaces {
|
|
if time.Since(prebuild.CreatedAt) > ttl {
|
|
expired = append(expired, prebuild)
|
|
} else {
|
|
nonExpired = append(nonExpired, prebuild)
|
|
}
|
|
}
|
|
return nonExpired, expired
|
|
}
|