Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add scheduling configuration for prebuilds#408

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
evgeniy-scherbina merged 19 commits intomainfromprebuilds-autoscaling-mechanism
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
19 commits
Select commitHold shift + click to select a range
d61894d
feat: add autoscaling configuration for prebuilds
evgeniy-scherbinaMay 29, 2025
7853727
fix: improve schedule validation
evgeniy-scherbinaJun 17, 2025
17b2adb
fix: allow DOM and Month fields
evgeniy-scherbinaJun 17, 2025
6403bc7
docs: improve documentation for timezone field
evgeniy-scherbinaJun 18, 2025
7bbe0d8
docs: make gen
evgeniy-scherbinaJun 18, 2025
5b0b1f9
Update provider/workspace_preset.go
evgeniy-scherbinaJun 18, 2025
4bd2f81
docs: improve doc comments
evgeniy-scherbinaJun 18, 2025
d19bea1
fix: tests
evgeniy-scherbinaJun 18, 2025
99680b0
refactor: rename autoscaling to scheduling
evgeniy-scherbinaJun 18, 2025
30979f0
docs: make gen
evgeniy-scherbinaJun 18, 2025
68bcb2f
refactor: minor refactor after renaming
evgeniy-scherbinaJun 18, 2025
ddd8b4c
Update provider/helpers/schedule_validation.go
evgeniy-scherbinaJun 18, 2025
1ae3fc7
Update provider/helpers/schedule_validation.go
evgeniy-scherbinaJun 18, 2025
201cd1d
refactor: improve docs
evgeniy-scherbinaJun 18, 2025
604cb1e
refactor: improve docs
evgeniy-scherbinaJun 18, 2025
9437525
test: improve test coverage
evgeniy-scherbinaJun 18, 2025
59bc618
test: improve test coverage
evgeniy-scherbinaJun 18, 2025
d040b81
refactor: check for a specific error in tests
evgeniy-scherbinaJun 18, 2025
79def30
refactor: check for a specific error in tests
evgeniy-scherbinaJun 18, 2025
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
20 changes: 20 additions & 0 deletionsdocs/data-sources/workspace_preset.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,10 +55,30 @@ Required:
Optional:

- `expiration_policy` (Block Set, Max: 1) Configuration block that defines TTL (time-to-live) behavior for prebuilds. Use this to automatically invalidate and delete prebuilds after a certain period, ensuring they stay up-to-date. (see [below for nested schema](#nestedblock--prebuilds--expiration_policy))
- `scheduling` (Block List, Max: 1) Configuration block that defines scheduling behavior for prebuilds. Use this to automatically adjust the number of prebuild instances based on a schedule. (see [below for nested schema](#nestedblock--prebuilds--scheduling))

<a id="nestedblock--prebuilds--expiration_policy"></a>
### Nested Schema for `prebuilds.expiration_policy`

Required:

- `ttl` (Number) Time in seconds after which an unclaimed prebuild is considered expired and eligible for cleanup.


<a id="nestedblock--prebuilds--scheduling"></a>
### Nested Schema for `prebuilds.scheduling`

Required:

- `schedule` (Block List, Min: 1) One or more schedule blocks that define when to scale the number of prebuild instances. (see [below for nested schema](#nestedblock--prebuilds--scheduling--schedule))
- `timezone` (String) The timezone to use for the prebuild schedules (e.g., "UTC", "America/New_York").
Timezone must be a valid timezone in the IANA timezone database.
See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a complete list of valid timezone identifiers and https://www.iana.org/time-zones for the official IANA timezone database.

<a id="nestedblock--prebuilds--scheduling--schedule"></a>
### Nested Schema for `prebuilds.scheduling.schedule`

Required:

- `cron` (String) A cron expression that defines when this schedule should be active. The cron expression must be in the format "* HOUR DOM MONTH DAY-OF-WEEK" where HOUR is 0-23, DOM (day-of-month) is 1-31, MONTH is 1-12, and DAY-OF-WEEK is 0-6 (Sunday-Saturday). The minute field must be "*" to ensure the schedule covers entire hours rather than specific minute intervals.
- `instances` (Number) The number of prebuild instances to maintain during this schedule period.
17 changes: 11 additions & 6 deletionsintegration/integration_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,12 +90,17 @@ func TestIntegration(t *testing.T) {
// TODO (sasswart): the cli doesn't support presets yet.
// once it does, the value for workspace_parameter.value
// will be the preset value.
"workspace_parameter.value": `param value`,
"workspace_parameter.icon": `param icon`,
"workspace_preset.name": `preset`,
"workspace_preset.parameters.param": `preset param value`,
"workspace_preset.prebuilds.instances": `1`,
"workspace_preset.prebuilds.expiration_policy.ttl": `86400`,
"workspace_parameter.value": `param value`,
"workspace_parameter.icon": `param icon`,
"workspace_preset.name": `preset`,
"workspace_preset.parameters.param": `preset param value`,
"workspace_preset.prebuilds.instances": `1`,
"workspace_preset.prebuilds.expiration_policy.ttl": `86400`,
"workspace_preset.prebuilds.scheduling.timezone": `UTC`,
"workspace_preset.prebuilds.scheduling.schedule0.cron": `\* 8-18 \* \* 1-5`,
"workspace_preset.prebuilds.scheduling.schedule0.instances": `3`,
"workspace_preset.prebuilds.scheduling.schedule1.cron": `\* 8-14 \* \* 6`,
"workspace_preset.prebuilds.scheduling.schedule1.instances": `1`,
},
},
{
Expand Down
16 changes: 16 additions & 0 deletionsintegration/test-data-source/main.tf
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,17 @@ data "coder_workspace_preset" "preset" {
expiration_policy {
ttl = 86400
}
scheduling {
timezone = "UTC"
schedule {
cron = "* 8-18 * * 1-5"
instances = 3
}
schedule {
cron = "* 8-14 * * 6"
instances = 1
}
}
}
}

Expand All@@ -56,6 +67,11 @@ locals {
"workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param,
"workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances),
"workspace_preset.prebuilds.expiration_policy.ttl" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).expiration_policy).ttl),
"workspace_preset.prebuilds.scheduling.timezone" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).scheduling).timezone),
"workspace_preset.prebuilds.scheduling.schedule0.cron" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).scheduling).schedule[0].cron),
"workspace_preset.prebuilds.scheduling.schedule0.instances" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).scheduling).schedule[0].instances),
"workspace_preset.prebuilds.scheduling.schedule1.cron" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).scheduling).schedule[1].cron),
"workspace_preset.prebuilds.scheduling.schedule1.instances" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).scheduling).schedule[1].instances),
}
}

Expand Down
187 changes: 187 additions & 0 deletionsprovider/helpers/schedule_validation.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
package helpers

import (
"strconv"
"strings"

"golang.org/x/xerrors"
)

// ValidateSchedules checks if any schedules overlap
func ValidateSchedules(schedules []string) error {
for i := 0; i < len(schedules); i++ {
for j := i + 1; j < len(schedules); j++ {
overlap, err := SchedulesOverlap(schedules[i], schedules[j])
if err != nil {
return xerrors.Errorf("invalid schedule: %w", err)
}
if overlap {
return xerrors.Errorf("schedules overlap: %s and %s",
schedules[i], schedules[j])
Comment on lines +19 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

This error message could be more helpful. We know which aspect of the schedules overlap, so let's help template authors out by telling them.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

It means everything (minutes, hours and days) overlap.

}
}
}
return nil
}

// SchedulesOverlap checks if two schedules overlap by checking
// all cron fields separately
func SchedulesOverlap(schedule1, schedule2 string) (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is excellent work@evgeniy-scherbina!

I really like how clean the code is, how complete the code-coverage, and how clear the intentions are.

evgeniy-scherbina reacted with hooray emojievgeniy-scherbina reacted with rocket emoji
// Get cron fields
fields1 := strings.Fields(schedule1)
fields2 := strings.Fields(schedule2)

if len(fields1) != 5 {
return false, xerrors.Errorf("schedule %q has %d fields, expected 5 fields (minute hour day-of-month month day-of-week)", schedule1, len(fields1))
}
if len(fields2) != 5 {
return false, xerrors.Errorf("schedule %q has %d fields, expected 5 fields (minute hour day-of-month month day-of-week)", schedule2, len(fields2))
}

// Check if months overlap
monthsOverlap, err := MonthsOverlap(fields1[3], fields2[3])
if err != nil {
return false, xerrors.Errorf("invalid month range: %w", err)
}
if !monthsOverlap {
return false, nil
}

// Check if days overlap (DOM OR DOW)
daysOverlap, err := DaysOverlap(fields1[2], fields1[4], fields2[2], fields2[4])
if err != nil {
return false, xerrors.Errorf("invalid day range: %w", err)
}
if !daysOverlap {
return false, nil
}

// Check if hours overlap
hoursOverlap, err := HoursOverlap(fields1[1], fields2[1])
if err != nil {
return false, xerrors.Errorf("invalid hour range: %w", err)
}

return hoursOverlap, nil
}

// MonthsOverlap checks if two month ranges overlap
func MonthsOverlap(months1, months2 string) (bool, error) {
return CheckOverlap(months1, months2, 12)
}

// HoursOverlap checks if two hour ranges overlap
func HoursOverlap(hours1, hours2 string) (bool, error) {
return CheckOverlap(hours1, hours2, 23)
}

// DomOverlap checks if two day-of-month ranges overlap
func DomOverlap(dom1, dom2 string) (bool, error) {
return CheckOverlap(dom1, dom2, 31)
}

// DowOverlap checks if two day-of-week ranges overlap
func DowOverlap(dow1, dow2 string) (bool, error) {
return CheckOverlap(dow1, dow2, 6)
}

// DaysOverlap checks if two day ranges overlap, considering both DOM and DOW.
// Returns true if both DOM and DOW overlap, or if one is * and the other overlaps.
func DaysOverlap(dom1, dow1, dom2, dow2 string) (bool, error) {
// If either DOM is *, we only need to check DOW overlap
if dom1 == "*" || dom2 == "*" {
return DowOverlap(dow1, dow2)
}

// If either DOW is *, we only need to check DOM overlap
if dow1 == "*" || dow2 == "*" {
return DomOverlap(dom1, dom2)
}

// If both DOM and DOW are specified, we need to check both
// because the schedule runs when either matches
domOverlap, err := DomOverlap(dom1, dom2)
if err != nil {
return false, err
}
dowOverlap, err := DowOverlap(dow1, dow2)
if err != nil {
return false, err
}

// If either DOM or DOW overlaps, the schedules overlap
return domOverlap || dowOverlap, nil
}

// CheckOverlap is a function to check if two ranges overlap
func CheckOverlap(range1, range2 string, maxValue int) (bool, error) {
set1, err := ParseRange(range1, maxValue)
if err != nil {
return false, err
}
set2, err := ParseRange(range2, maxValue)
if err != nil {
return false, err
}

for value := range set1 {
if set2[value] {
return true, nil
}
}
return false, nil
}

// ParseRange converts a cron range to a set of integers
// maxValue is the maximum allowed value (e.g., 23 for hours, 6 for DOW, 12 for months, 31 for DOM)
func ParseRange(input string, maxValue int) (map[int]bool, error) {
result := make(map[int]bool)

// Handle "*" case
if input == "*" {
for i := 0; i <= maxValue; i++ {
result[i] = true
}
return result, nil
}

// Parse ranges like "1-3,5,7-9"
parts := strings.Split(input, ",")
for _, part := range parts {
if strings.Contains(part, "-") {
// Handle range like "1-3"
rangeParts := strings.Split(part, "-")
start, err := strconv.Atoi(rangeParts[0])
if err != nil {
return nil, xerrors.Errorf("invalid start value in range: %w", err)
}
end, err := strconv.Atoi(rangeParts[1])
if err != nil {
return nil, xerrors.Errorf("invalid end value in range: %w", err)
}

// Validate range
if start < 0 || end > maxValue || start > end {
return nil, xerrors.Errorf("invalid range %d-%d: values must be between 0 and %d", start, end, maxValue)
}

for i := start; i <= end; i++ {
result[i] = true
}
} else {
// Handle single value
value, err := strconv.Atoi(part)
if err != nil {
return nil, xerrors.Errorf("invalid value: %w", err)
}

// Validate value
if value < 0 || value > maxValue {
return nil, xerrors.Errorf("invalid value %d: must be between 0 and %d", value, maxValue)
}

result[value] = true
}
}
return result, nil
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp