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

fix(manifest): preserve default values on environment overrides for jobs #2044

Merged
merged 1 commit into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 15 additions & 14 deletions internal/pkg/deploy/cloudformation/stack/scheduled_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,32 +203,33 @@ func (j *ScheduledJob) SerializedParameters() (string, error) {
// Exception is made for strings of the form "rate( )" or "cron( )". These are accepted as-is and
// validated server-side by CloudFormation.
func (j *ScheduledJob) awsSchedule() (string, error) {
if j.manifest.On.Schedule == "" {
schedule := aws.StringValue(j.manifest.On.Schedule)
if schedule == "" {
return "", fmt.Errorf(`missing required field "schedule" in manifest for job %s`, j.name)
}
// If the schedule uses default CloudWatch Events syntax, pass it through for server-side validation.
if match := awsScheduleRegexp.FindStringSubmatch(j.manifest.On.Schedule); match != nil {
return j.manifest.On.Schedule, nil
if match := awsScheduleRegexp.FindStringSubmatch(schedule); match != nil {
return aws.StringValue(j.manifest.On.Schedule), nil
}
// Try parsing the string as a cron expression to validate it.
if _, err := cron.ParseStandard(j.manifest.On.Schedule); err != nil {
if _, err := cron.ParseStandard(schedule); err != nil {
return "", errScheduleInvalid{reason: err}
}
var scheduleExpression string
var err error
switch {
case strings.HasPrefix(j.manifest.On.Schedule, every):
scheduleExpression, err = toRate(j.manifest.On.Schedule[len(every):])
case strings.HasPrefix(schedule, every):
scheduleExpression, err = toRate(schedule[len(every):])
if err != nil {
return "", fmt.Errorf("parse fixed interval: %w", err)
}
case strings.HasPrefix(j.manifest.On.Schedule, "@"):
scheduleExpression, err = toFixedSchedule(j.manifest.On.Schedule)
case strings.HasPrefix(schedule, "@"):
scheduleExpression, err = toFixedSchedule(schedule)
if err != nil {
return "", fmt.Errorf("parse preset schedule: %w", err)
}
default:
scheduleExpression, err = toAWSCron(j.manifest.On.Schedule)
scheduleExpression, err = toAWSCron(schedule)
if err != nil {
return "", fmt.Errorf("parse cron schedule: %w", err)
}
Expand Down Expand Up @@ -357,8 +358,8 @@ func toAWSCron(schedule string) (string, error) {
// It also performs basic validations to provide a fast feedback loop to the customer.
func (j *ScheduledJob) stateMachineOpts() (*template.StateMachineOpts, error) {
var timeoutSeconds *int
if j.manifest.Timeout != "" {
parsedTimeout, err := time.ParseDuration(j.manifest.Timeout)
if inTimeout := aws.StringValue(j.manifest.Timeout); inTimeout != "" {
parsedTimeout, err := time.ParseDuration(inTimeout)
if err != nil {
return nil, errDurationInvalid{reason: err}
}
Expand All @@ -372,11 +373,11 @@ func (j *ScheduledJob) stateMachineOpts() (*template.StateMachineOpts, error) {
}

var retries *int
if j.manifest.Retries != 0 {
if j.manifest.Retries < 0 {
if inRetries := aws.IntValue(j.manifest.Retries); inRetries != 0 {
if inRetries < 0 {
return nil, errors.New("number of retries cannot be negative")
}
retries = aws.Int(j.manifest.Retries)
retries = aws.Int(inRetries)
}
return &template.StateMachineOpts{
Timeout: timeoutSeconds,
Expand Down
14 changes: 7 additions & 7 deletions internal/pkg/deploy/cloudformation/stack/scheduled_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ func TestScheduledJob_Template(t *testing.T) {
Retries: 3,
})
testScheduledJobManifest.EntryPoint = manifest.EntryPointOverride{
String: nil,
String: nil,
StringSlice: []string{"/bin/echo", "hello"},
}
testScheduledJobManifest.Command = manifest.CommandOverride{
String: nil,
String: nil,
StringSlice: []string{"world"},
}

Expand All @@ -66,7 +66,7 @@ func TestScheduledJob_Template(t *testing.T) {
SubnetsType: template.PublicSubnetsPlacement,
},
EntryPoint: []string{"/bin/echo", "hello"},
Command: []string{"world"},
Command: []string{"world"},
})).Return(&template.Content{Buffer: bytes.NewBufferString("template")}, nil)
addons := mockTemplater{err: &addon.ErrAddonsDirNotExist{}}
j.parser = m
Expand Down Expand Up @@ -94,7 +94,7 @@ func TestScheduledJob_Template(t *testing.T) {
SubnetsType: template.PublicSubnetsPlacement,
},
EntryPoint: []string{"/bin/echo", "hello"},
Command: []string{"world"},
Command: []string{"world"},
})).Return(&template.Content{Buffer: bytes.NewBufferString("template")}, nil)
addons := mockTemplater{
tpl: `Resources:
Expand Down Expand Up @@ -305,7 +305,7 @@ func TestScheduledJob_awsSchedule(t *testing.T) {
manifest: &manifest.ScheduledJob{
ScheduledJobConfig: manifest.ScheduledJobConfig{
On: manifest.JobTriggerConfig{
Schedule: tc.inputSchedule,
Schedule: aws.String(tc.inputSchedule),
},
},
},
Expand Down Expand Up @@ -385,8 +385,8 @@ func TestScheduledJob_stateMachine(t *testing.T) {
manifest: &manifest.ScheduledJob{
ScheduledJobConfig: manifest.ScheduledJobConfig{
JobFailureHandlerConfig: manifest.JobFailureHandlerConfig{
Retries: tc.inputRetries,
Timeout: tc.inputTimeout,
Retries: aws.Int(tc.inputRetries),
Timeout: aws.String(tc.inputTimeout),
},
},
},
Expand Down
31 changes: 21 additions & 10 deletions internal/pkg/manifest/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ type ScheduledJobConfig struct {

// JobTriggerConfig represents the configuration for the event that triggers the job.
type JobTriggerConfig struct {
Schedule string `yaml:"schedule"`
Schedule *string `yaml:"schedule"`
}

// JobFailureHandlerConfig represents the error handling configuration for the job.
type JobFailureHandlerConfig struct {
Timeout string `yaml:"timeout"`
Retries int `yaml:"retries"`
Timeout *string `yaml:"timeout"`
Retries *int `yaml:"retries"`
}

// ScheduledJobProps contains properties for creating a new scheduled job manifest.
Expand Down Expand Up @@ -93,13 +93,24 @@ func newDefaultScheduledJob() *ScheduledJob {
func NewScheduledJob(props *ScheduledJobProps) *ScheduledJob {
job := newDefaultScheduledJob()
// Apply overrides.
job.Name = aws.String(props.Name)
job.ScheduledJobConfig.ImageConfig.Build.BuildArgs.Dockerfile = stringP(props.Dockerfile)
job.ScheduledJobConfig.ImageConfig.Location = stringP(props.Image)
job.On.Schedule = props.Schedule
job.Retries = props.Retries
job.Timeout = props.Timeout

if props.Name != "" {
job.Name = aws.String(props.Name)
}
if props.Dockerfile != "" {
job.ScheduledJobConfig.ImageConfig.Build.BuildArgs.Dockerfile = aws.String(props.Dockerfile)
}
if props.Image != "" {
job.ScheduledJobConfig.ImageConfig.Location = aws.String(props.Image)
}
if props.Schedule != "" {
job.On.Schedule = aws.String(props.Schedule)
}
if props.Retries != 0 {
job.Retries = aws.Int(props.Retries)
}
if props.Timeout != "" {
job.Timeout = aws.String(props.Timeout)
}
Comment on lines +96 to +113
Copy link
Contributor

Choose a reason for hiding this comment

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

o_O thank you so much for cleaning up this craziness

job.parser = template.New()
return job
}
Expand Down
110 changes: 110 additions & 0 deletions internal/pkg/manifest/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -82,3 +83,112 @@ func TestScheduledJob_MarshalBinary(t *testing.T) {
})
}
}

func TestScheduledJob_ApplyEnv(t *testing.T) {
testCases := map[string]struct {
inputManifest *ScheduledJob
inputEnv string

wantedManifest *ScheduledJob
wantedErr error
}{
"should return the same scheduled job if the environment does not exist": {
inputManifest: newDefaultScheduledJob(),
inputEnv: "test",

wantedManifest: newDefaultScheduledJob(),
},
"should preserve defaults and only override fields under 'environment'": {
inputManifest: &ScheduledJob{
Workload: Workload{
Name: aws.String("report-generator"),
Type: aws.String(ScheduledJobType),
},
ScheduledJobConfig: ScheduledJobConfig{
ImageConfig: Image{
Location: aws.String("nginx"),
},
On: JobTriggerConfig{
Schedule: aws.String("@hourly"),
},
JobFailureHandlerConfig: JobFailureHandlerConfig{
Timeout: aws.String("5m"),
Retries: aws.Int(1),
},
TaskConfig: TaskConfig{
CPU: aws.Int(256),
Memory: aws.Int(512),
Count: Count{
Value: aws.Int(1),
},
},
Network: NetworkConfig{
VPC: vpcConfig{
Placement: stringP(PublicSubnetPlacement),
},
},
},
Environments: map[string]*ScheduledJobConfig{
"prod": {
TaskConfig: TaskConfig{
Variables: map[string]string{
"LOG_LEVEL": "prod",
},
},
},
},
},
inputEnv: "prod",

wantedManifest: &ScheduledJob{
Workload: Workload{
Name: aws.String("report-generator"),
Type: aws.String(ScheduledJobType),
},
ScheduledJobConfig: ScheduledJobConfig{
ImageConfig: Image{
Location: aws.String("nginx"),
},
On: JobTriggerConfig{
Schedule: aws.String("@hourly"),
},
JobFailureHandlerConfig: JobFailureHandlerConfig{
Timeout: aws.String("5m"),
Retries: aws.Int(1),
},
TaskConfig: TaskConfig{
CPU: aws.Int(256),
Memory: aws.Int(512),
Count: Count{
Value: aws.Int(1),
},
Variables: map[string]string{
"LOG_LEVEL": "prod",
},
},
Network: NetworkConfig{
VPC: vpcConfig{
Placement: stringP(PublicSubnetPlacement),
},
},
},
Environments: nil,
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// WHEN
actualManifest, actualErr := tc.inputManifest.ApplyEnv(tc.inputEnv)

// THEN
if tc.wantedErr != nil {
require.EqualError(t, actualErr, tc.wantedErr.Error())
} else {
require.NoError(t, actualErr)
require.Equal(t, tc.wantedManifest, actualManifest)
}
})
}
}