Skip to content
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

feat: Adds e2e tests for chain/schedule policy and bump ScheduledAutoscaler to Alpha #3946

Merged
merged 20 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3f5bc57
setup e2e tests for chain/schedule policies
indexjoseph Aug 8, 2024
a61dfa7
add default fixtures for schedule & chain fas
indexjoseph Aug 8, 2024
7e917f8
Adds e2e tests for chain and schedule policies
indexjoseph Aug 13, 2024
f25dc03
Merge branch 'main' into e2e-chain-schedule-policy
indexjoseph Aug 13, 2024
2131907
Merge main
indexjoseph Aug 14, 2024
b727077
Revert "Merge main"
indexjoseph Aug 14, 2024
5a93315
Merge branch 'main' into e2e-chain-schedule-policy
indexjoseph Aug 14, 2024
c2d775b
Bump ScheduledAutoscaler feature from Dev to Alpha
indexjoseph Aug 15, 2024
db2d7d5
Make feature gate inverse in cloudbuild.yaml
indexjoseph Aug 15, 2024
d790c7a
Merge branch 'main' into e2e-chain-schedule-policy
gongmax Aug 16, 2024
cfc4bd2
Fix feature gates ordering and address duration error
indexjoseph Aug 21, 2024
770b630
Remove den-install feature gate
indexjoseph Aug 22, 2024
68045e3
Remove den-install feature gate
indexjoseph Aug 22, 2024
cae25ce
Update test/e2e/fleetautoscaler_test.go
indexjoseph Aug 22, 2024
3f726d2
Merge branch 'main' into e2e-chain-schedule-policy
indexjoseph Aug 22, 2024
454410a
Merge branch 'main' into e2e-chain-schedule-policy
indexjoseph Aug 23, 2024
af51544
Merge branch 'main' into e2e-chain-schedule-policy
indexjoseph Aug 23, 2024
709a0df
Regenerate install.yaml
indexjoseph Aug 23, 2024
9ad8246
Merge branch 'main' into e2e-chain-schedule-policy
igooch Aug 23, 2024
c747d46
Move ScheduledAutoscaler feature stage into 1.43 version
indexjoseph Aug 24, 2024
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
4 changes: 3 additions & 1 deletion build/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ GS_TEST_IMAGE ?= us-docker.pkg.dev/agones-images/examples/simple-game-server:0.3
BETA_FEATURE_GATES ?= "AutopilotPassthroughPort=true&CountsAndLists=true&DisableResyncOnSDKServer=true"

# Enable all alpha feature gates. Keep in sync with `false` (alpha) entries in pkg/util/runtime/features.go:featureDefaults
ALPHA_FEATURE_GATES ?= "PlayerAllocationFilter=true&PlayerTracking=true&RollingUpdateFix=true&PortRanges=true&PortPolicyNone=true&Example=true"
ALPHA_FEATURE_GATES ?= "PlayerAllocationFilter=true&PlayerTracking=true&RollingUpdateFix=true&PortRanges=true&PortPolicyNone=true&ScheduledAutoscaler=true&Example=true"

# Build with Windows support
WITH_WINDOWS=1
Expand Down Expand Up @@ -729,13 +729,15 @@ gen-embedded-openapi: ensure-build-image
go run -mod=mod ./main.go

# Generate the static install script
gen-install: FEATURE_GATES ?= $(ALPHA_FEATURE_GATES)
indexjoseph marked this conversation as resolved.
Show resolved Hide resolved
gen-install: $(ensure-build-image)
docker run --rm $(common_mounts) $(DOCKER_RUN_ARGS) $(build_tag) bash -c \
'helm template agones-manual --namespace agones-system $(mount_path)/install/helm/agones \
--set agones.controller.generateTLS=false \
--set agones.allocator.generateTLS=false \
--set agones.allocator.generateClientTLS=false \
--set agones.crds.cleanupOnDelete=false \
--set agones.featureGates=$(FEATURE_GATES) \
indexjoseph marked this conversation as resolved.
Show resolved Hide resolved
> $(mount_path)/install/yaml/install.yaml'

# Generate the client, conversions, deepcopy, and defaults code for our CustomResourceDefinition
Expand Down
2 changes: 1 addition & 1 deletion cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ steps:
declare -A versionsAndRegions=( [1.27]=us-east1 [1.28]=us-west1 [1.29]=europe-west1 )

# Keep in sync with (the inverse of) pkg/util/runtime/features.go:featureDefaults
featureWithGate="PlayerAllocationFilter=true&PlayerTracking=true&CountsAndLists=false&RollingUpdateFix=true&PortRanges=true&PortPolicyNone=true&DisableResyncOnSDKServer=false&AutopilotPassthroughPort=false&Example=true"
featureWithGate="PlayerAllocationFilter=true&PlayerTracking=true&CountsAndLists=false&RollingUpdateFix=true&PortRanges=true&PortPolicyNone=true&ScheduledAutoscaler=true&DisableResyncOnSDKServer=false&AutopilotPassthroughPort=false&Example=true"
featureWithoutGate=""

# Use this if specific feature gates can only be supported on specific Kubernetes versions.
Expand Down
2 changes: 1 addition & 1 deletion install/helm/agones/defaultfeaturegates.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ PlayerTracking: false
RollingUpdateFix: false
PortRanges: false
PortPolicyNone: false
ScheduledAutoscaler: false

# Dev features
ScheduledAutoscaler: false

# Example feature
Example: false
347 changes: 343 additions & 4 deletions install/yaml/install.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/fleetautoscalers/fleetautoscalers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2538,7 +2538,7 @@ func TestApplyChainPolicy(t *testing.T) {
// Parse a time string and return a metav1.Time
func mustParseMetav1Time(timeStr string) metav1.Time {
t, _ := time.Parse(time.RFC3339, timeStr)
return metav1.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
return metav1.NewTime(t)
}

// Parse a time string and return a time.Time
Expand Down
12 changes: 6 additions & 6 deletions pkg/util/runtime/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ const (
// FeaturePlayerTracking is a feature flag to enable/disable player tracking features.
FeaturePlayerTracking Feature = "PlayerTracking"

// FeatureRollingUpdateFix is a feature flag to enable/disable fleet controller fixes.
FeatureRollingUpdateFix Feature = "RollingUpdateFix"

// FeaturePortRanges is a feature flag to enable/disable specific port ranges.
FeaturePortRanges Feature = "PortRanges"

// FeaturePortPolicyNone is a feature flag to allow setting Port Policy to None.
FeaturePortPolicyNone Feature = "PortPolicyNone"

////////////////
// Dev features
// FeatureRollingUpdateFix is a feature flag to enable/disable fleet controller fixes.
FeatureRollingUpdateFix Feature = "RollingUpdateFix"

// FeatureScheduledAutoscaler is a feature flag to enable/disable scheduled fleet autoscaling.
FeatureScheduledAutoscaler Feature = "ScheduledAutoscaler"

////////////////
// Dev features

////////////////
// Example feature

Expand Down Expand Up @@ -139,9 +139,9 @@ var (
FeatureRollingUpdateFix: false,
indexjoseph marked this conversation as resolved.
Show resolved Hide resolved
FeaturePortRanges: false,
FeaturePortPolicyNone: false,
FeatureScheduledAutoscaler: false,
indexjoseph marked this conversation as resolved.
Show resolved Hide resolved

// Dev features
FeatureScheduledAutoscaler: false,

// Example feature
FeatureExample: false,
Expand Down
2 changes: 1 addition & 1 deletion site/content/en/docs/Guides/feature-stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The current set of `alpha` and `beta` feature gates:
| Example Gate (not in use) | `Example` | Disabled | None | 0.13.0 |

{{% /feature %}}

| [Scheduled Fleet Autoscaling](https://github.com/googleforgames/agones/issues/3008) | `ScheduledAutoscaler` | Disabled | `Alpha` | 1.43.0 |
indexjoseph marked this conversation as resolved.
Show resolved Hide resolved
{{% feature publishVersion="1.43.0" %}}

| Feature Name | Gate | Default | Stage | Since |
Expand Down
254 changes: 254 additions & 0 deletions test/e2e/fleetautoscaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1445,3 +1445,257 @@ func TestListAutoscalerWithSDKMethods(t *testing.T) {
})
}
}

func TestScheduleAutoscaler(t *testing.T) {
if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) {
t.SkipNow()
}
t.Parallel()
ctx := context.Background()
log := e2e.TestLogger(t)

stable := framework.AgonesClient.AgonesV1()
fleets := stable.Fleets(framework.Namespace)
flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{})
if assert.NoError(t, err) {
defer fleets.Delete(context.Background(), flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck
}

framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas))

fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace)

// Active Cron Schedule (e.g. run after 1 * * * *, which is the after the first minute of the hour)
scheduleAutoscaler := defaultAutoscalerSchedule(t, flt)
scheduleAutoscaler.Spec.Policy.Schedule.ActivePeriod.StartCron = nextCronMinute(time.Now())
fas, err := fleetautoscalers.Create(ctx, scheduleAutoscaler, metav1.CreateOptions{})
assert.NoError(t, err)

framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(5))
fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck

// Return to starting 3 replicas
framework.ScaleFleet(t, log, flt, 3)
framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(3))

// Between Active Period Cron Schedule (e.g. run between 1-2 * * * *, which is between the first minute and second minute of the hour)
scheduleAutoscaler = defaultAutoscalerSchedule(t, flt)
scheduleAutoscaler.Spec.Policy.Schedule.ActivePeriod.StartCron = nextCronMinuteBetween(time.Now())
fas, err = fleetautoscalers.Create(ctx, scheduleAutoscaler, metav1.CreateOptions{})
assert.NoError(t, err)

framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(5))
fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck
}

func TestChainAutoscaler(t *testing.T) {
if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) {
t.SkipNow()
}
t.Parallel()
ctx := context.Background()
log := e2e.TestLogger(t)

stable := framework.AgonesClient.AgonesV1()
fleets := stable.Fleets(framework.Namespace)
flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{})
if assert.NoError(t, err) {
defer fleets.Delete(context.Background(), flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck
}

framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas))

fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace)

// 1st Schedule Inactive, 2nd Schedule Active - 30 seconds (Fallthrough)
chainAutoscaler := defaultAutoscalerChain(t, flt)
fas, err := fleetautoscalers.Create(ctx, chainAutoscaler, metav1.CreateOptions{})
assert.NoError(t, err)

// Verify only the second schedule ran
framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(4))
fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck

// Return to starting 3 replicas
framework.ScaleFleet(t, log, flt, 3)
framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(3))

// 2 Active Schedules back to back - 1 minute (Fallthrough)
chainAutoscaler = defaultAutoscalerChain(t, flt)
currentTime := time.Now()

// First schedule runs for 1 minute
chainAutoscaler.Spec.Policy.Chain[0].Schedule.ActivePeriod.StartCron = nextCronMinute(currentTime)
chainAutoscaler.Spec.Policy.Chain[0].Schedule.ActivePeriod.Duration = "1m"

// Second schedule runs 1 minute after the first schedule
oneMinute := mustParseDuration(t, "1m")
chainAutoscaler.Spec.Policy.Chain[0].Schedule.ActivePeriod.StartCron = nextCronMinute(currentTime.Add(oneMinute))
chainAutoscaler.Spec.Policy.Chain[1].Schedule.ActivePeriod.Duration = "5m"

fas, err = fleetautoscalers.Create(ctx, chainAutoscaler, metav1.CreateOptions{})
assert.NoError(t, err)

// Verify the first schedule has been applied
framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(10))
// Verify the second schedule has been applied
framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(4))

fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck
}

// defaultAutoscalerSchedule returns a default scheduled autoscaler for testing.
func defaultAutoscalerSchedule(t *testing.T, f *agonesv1.Fleet) *autoscalingv1.FleetAutoscaler {
return &autoscalingv1.FleetAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: f.ObjectMeta.Name + "-scheduled-autoscaler",
Namespace: framework.Namespace,
},
Spec: autoscalingv1.FleetAutoscalerSpec{
FleetName: f.ObjectMeta.Name,
Policy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.SchedulePolicyType,
Schedule: &autoscalingv1.SchedulePolicy{
Between: autoscalingv1.Between{
Start: currentTimePlusDuration(t, "1s"),
End: currentTimePlusDuration(t, "1m"),
},
ActivePeriod: autoscalingv1.ActivePeriod{
Timezone: "UTC",
StartCron: "* * * * *",
Duration: "",
},
Policy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.BufferPolicyType,
Buffer: &autoscalingv1.BufferPolicy{
BufferSize: intstr.FromInt(5),
MinReplicas: 5,
MaxReplicas: 12,
},
},
},
},
Sync: &autoscalingv1.FleetAutoscalerSync{
Type: autoscalingv1.FixedIntervalSyncType,
FixedInterval: autoscalingv1.FixedIntervalSync{
Seconds: 5,
},
},
},
}
}

// defaultAutoscalerChain returns a default chain autoscaler for testing.
func defaultAutoscalerChain(t *testing.T, f *agonesv1.Fleet) *autoscalingv1.FleetAutoscaler {
return &autoscalingv1.FleetAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: f.ObjectMeta.Name + "-chain-autoscaler",
Namespace: framework.Namespace,
},
Spec: autoscalingv1.FleetAutoscalerSpec{
FleetName: f.ObjectMeta.Name,
Policy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.ChainPolicyType,
Chain: autoscalingv1.ChainPolicy{
{
ID: "schedule-1",
FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.SchedulePolicyType,
Schedule: &autoscalingv1.SchedulePolicy{
Between: autoscalingv1.Between{
Start: currentTimePlusDuration(t, "1s"),
End: currentTimePlusDuration(t, "2m"),
},
ActivePeriod: autoscalingv1.ActivePeriod{
Timezone: "",
StartCron: inactiveCronSchedule(time.Now()),
Duration: "1m",
},
Policy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.BufferPolicyType,
Buffer: &autoscalingv1.BufferPolicy{
BufferSize: intstr.FromInt(10),
MinReplicas: 10,
MaxReplicas: 20,
},
},
},
},
},
{
ID: "schedule-2",
FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.SchedulePolicyType,
Schedule: &autoscalingv1.SchedulePolicy{
Between: autoscalingv1.Between{
Start: currentTimePlusDuration(t, "1s"),
End: currentTimePlusDuration(t, "5m"),
},
ActivePeriod: autoscalingv1.ActivePeriod{
Timezone: "",
StartCron: nextCronMinute(time.Now()),
Duration: "",
},
Policy: autoscalingv1.FleetAutoscalerPolicy{
Type: autoscalingv1.BufferPolicyType,
Buffer: &autoscalingv1.BufferPolicy{
BufferSize: intstr.FromInt(4),
MinReplicas: 3,
MaxReplicas: 7,
},
},
},
},
},
},
},
Sync: &autoscalingv1.FleetAutoscalerSync{
Type: autoscalingv1.FixedIntervalSyncType,
FixedInterval: autoscalingv1.FixedIntervalSync{
Seconds: 5,
},
},
},
}
}

// inactiveCronSchedule returns the time 3 minutes ago
// e.g. if the current time is 12:00, this method will return "57 * * * *"
// meaning 3 minutes before 12:00
func inactiveCronSchedule(currentTime time.Time) string {
prevMinute := currentTime.Add(time.Minute * -3).Minute()
return fmt.Sprintf("%d * * * *", prevMinute)
}

// nextCronMinute returns the very next minute in
// e.g. if the current time is 12:00, this method will return "1 * * * *"
// meaning after 12:01
func nextCronMinute(currentTime time.Time) string {
nextMinute := currentTime.Add(time.Minute).Minute()
return fmt.Sprintf("%d * * * *", nextMinute)
}

// nextCronMinuteBetween returns the minute between the very next minute
// e.g. if the current time is 12:00, this method will return "1-2 * * * *"
// meaning between 12:01 - 12:02
func nextCronMinuteBetween(currentTime time.Time) string {
nextMinute := currentTime.Add(time.Minute).Minute()
secondMinute := currentTime.Add(2 * time.Minute).Minute()
return fmt.Sprintf("%d-%d * * * *", nextMinute, secondMinute)
}

// Parse a duration string and return a duration struct
func mustParseDuration(t *testing.T, duration string) time.Duration {
d, err := time.ParseDuration(duration)
if !assert.Nil(t, err) {
fmt.Errorf("error parsing duration: %s", err)
}
indexjoseph marked this conversation as resolved.
Show resolved Hide resolved
return d
}

// Parse a time string and return a metav1.Time
func currentTimePlusDuration(t *testing.T, duration string) metav1.Time {
d := mustParseDuration(t, duration)
currentTimePlusDuration := time.Now().Add(d)
return metav1.NewTime(currentTimePlusDuration)
}
Loading