diff --git a/pipeline/builder/builder.go b/pipeline/builder/builder.go index 48eaeec..a92d1ef 100644 --- a/pipeline/builder/builder.go +++ b/pipeline/builder/builder.go @@ -128,6 +128,20 @@ func (b *Builder) Pipeline() (*types.SpinnakerPipeline, error) { Default: param.Default, Required: param.Required, } + + if len(param.Options) > 0 { + sp.Parameters[i].HasOptions = true + foundDefaultValue := param.Default == "" + for _, val := range param.Options { + foundDefaultValue = foundDefaultValue || param.Default == val.Value + sp.Parameters[i].Options = append(sp.Parameters[i].Options, types.Option{ + Value: val.Value, + }) + } + if !foundDefaultValue { + return sp, errors.New("builder: the specified default value is not one of the options") + } + } } var stageIndex = 0 @@ -442,7 +456,7 @@ func (b *Builder) defaultManifestStage(index int, s config.Stage) *types.Manifes markUnstableAsSuccessful := setDefaultIfNil(s.DeployEmbeddedManifests.MarkUnstableAsSuccessful, false) waitForCompletion := setDefaultIfNil(s.DeployEmbeddedManifests.WaitForCompletion, true) - return &types.ManifestStage{ + stage := &types.ManifestStage{ StageMetadata: buildStageMetadata(s, "deployManifest", index, b.isLinear), Account: s.Account, CloudProvider: "kubernetes", @@ -460,6 +474,13 @@ func (b *Builder) defaultManifestStage(index int, s config.Stage) *types.Manifes MarkUnstableAsSuccessful: &markUnstableAsSuccessful, WaitForCompletion: &waitForCompletion, } + + if s.DeployEmbeddedManifests.StageTimeoutMS > 0 { + stage.OverrideTimeout = true + stage.StageTimeoutMS = s.DeployEmbeddedManifests.StageTimeoutMS + } + + return stage } func (b *Builder) buildV2RunJobStage(index int, s config.Stage) (*types.ManifestStage, error) { diff --git a/pipeline/builder/builder_test.go b/pipeline/builder/builder_test.go index 2482064..a36230e 100644 --- a/pipeline/builder/builder_test.go +++ b/pipeline/builder/builder_test.go @@ -148,28 +148,173 @@ func TestBuilderPipelineStages(t *testing.T) { }) t.Run("Parameter configuration is parsed correctly", func(t *testing.T) { - pipeline := &config.Pipeline{ - Parameters: []config.Parameter{ - { - Name: "param1", - Description: "parameter description", - Default: "default value", - Required: true, + t.Run("Without Options", func(t *testing.T) { + pipeline := &config.Pipeline{ + Parameters: []config.Parameter{ + { + Name: "param1", + Description: "parameter description", + Default: "default value", + Required: true, + }, }, - }, - } + } + + b := builder.New(pipeline) + spinnaker, err := b.Pipeline() + require.NoError(t, err, "error generating pipeline json") + + require.Len(t, spinnaker.Parameters, 1) + + param := spinnaker.Parameters[0] + assert.Equal(t, true, param.Required) + assert.Equal(t, "parameter description", param.Description) + assert.Equal(t, "param1", param.Name) + assert.Equal(t, "default value", param.Default) + }) + + t.Run("With options", func(t *testing.T) { + pipeline := &config.Pipeline{ + Parameters: []config.Parameter{ + { + Name: "param1", + Description: "parameter description", + Required: true, + Options: []config.Option{ + { + Value: "opt1", + }, + { + Value: "opt2", + }, + }, + }, + }, + } + + b := builder.New(pipeline) + spinnaker, err := b.Pipeline() + require.NoError(t, err, "error generating pipeline json") - b := builder.New(pipeline) - spinnaker, err := b.Pipeline() - require.NoError(t, err, "error generating pipeline json") + require.Len(t, spinnaker.Parameters, 1) - require.Len(t, spinnaker.Parameters, 1) + param := spinnaker.Parameters[0] + assert.Equal(t, true, param.Required) + assert.Equal(t, "parameter description", param.Description) + assert.Equal(t, "param1", param.Name) + assert.True(t, param.HasOptions) + assert.Equal(t, "opt1", param.Options[0].Value) + assert.Equal(t, "opt2", param.Options[1].Value) + }) + + t.Run("With options and a default value", func(t *testing.T) { + pipeline := &config.Pipeline{ + Parameters: []config.Parameter{ + { + Name: "param1", + Description: "parameter description", + Default: "opt1", + Required: true, + Options: []config.Option{ + { + Value: "opt1", + }, + { + Value: "opt2", + }, + }, + }, + }, + } + + b := builder.New(pipeline) + spinnaker, err := b.Pipeline() + require.NoError(t, err, "error generating pipeline json") + + require.Len(t, spinnaker.Parameters, 1) + + param := spinnaker.Parameters[0] + assert.Equal(t, true, param.Required) + assert.Equal(t, "parameter description", param.Description) + assert.Equal(t, "param1", param.Name) + assert.Equal(t, "opt1", param.Default) + assert.True(t, param.HasOptions) + assert.Equal(t, "opt1", param.Options[0].Value) + assert.Equal(t, "opt2", param.Options[1].Value) + }) + + t.Run("With error handling for mismatched option and default values", func(t *testing.T) { + pipeline := &config.Pipeline{ + Parameters: []config.Parameter{ + { + Name: "param1", + Default: "optN", + Options: []config.Option{ + { + Value: "opt1", + }, + { + Value: "opt2", + }, + }, + }, + }, + } - param := spinnaker.Parameters[0] - assert.Equal(t, true, param.Required) - assert.Equal(t, "parameter description", param.Description) - assert.Equal(t, "param1", param.Name) - assert.Equal(t, "default value", param.Default) + b := builder.New(pipeline) + _, err := b.Pipeline() + require.Error(t, err, "builder: the specified default value is not one of the options") + }) + }) + + t.Run("DeployEmbeddedManifests is parsed correctly", func(t *testing.T) { + t.Run("Picks up files to deploy", func(t *testing.T) { + pipeline := &config.Pipeline{ + Stages: []config.Stage{ + { + Name: "Test DeployEmbeddedManifests Stage", + DeployEmbeddedManifests: &config.DeployEmbeddedManifests{ + + Files: []config.ManifestFile{ + { + File: file, + }, + }, + }, + }, + }, + } + builder := builder.New(pipeline) + spinnaker, err := builder.Pipeline() + require.NoError(t, err, "error generating pipeline json") + + assert.Equal(t, "Test DeployEmbeddedManifests Stage", spinnaker.Stages[0].(*types.ManifestStage).Name) + assert.NotNil(t, spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) + }) + + t.Run("Overrides default timeout", func(t *testing.T) { + pipeline := &config.Pipeline{ + Stages: []config.Stage{ + { + Name: "Test DeployEmbeddedManifests Stage", + DeployEmbeddedManifests: &config.DeployEmbeddedManifests{ + StageTimeoutMS: 360000, + Files: []config.ManifestFile{ + { + File: file, + }, + }, + }, + }, + }, + } + builder := builder.New(pipeline) + spinnaker, err := builder.Pipeline() + require.NoError(t, err, "error generating pipeline json") + + assert.Equal(t, int64(360000), spinnaker.Stages[0].(*types.ManifestStage).StageTimeoutMS) + assert.Equal(t, true, spinnaker.Stages[0].(*types.ManifestStage).OverrideTimeout) + }) }) t.Run("Deploy stage is parsed correctly", func(t *testing.T) { @@ -296,6 +441,27 @@ func TestBuilderPipelineStages(t *testing.T) { }) t.Run("ManualJudgement stage is parsed correctly", func(t *testing.T) { + t.Run("Override timeout is assigned", func(t *testing.T) { + pipeline := &config.Pipeline{ + Stages: []config.Stage{ + { + Name: "Test ManualJudgementStage Timeout", + ManualJudgement: &config.ManualJudgementStage{ + Timeout: 1, + }, + }, + }, + } + + builder := builder.New(pipeline) + spinnaker, err := builder.Pipeline() + require.NoError(t, err, "error generating pipeline json") + + assert.Equal(t, "Test ManualJudgementStage Timeout", spinnaker.Stages[0].(*types.ManualJudgementStage).Name) + assert.True(t, spinnaker.Stages[0].(*types.ManualJudgementStage).OverrideTimeout) + assert.Equal(t, int64(3600000), spinnaker.Stages[0].(*types.ManualJudgementStage).StageTimeoutMS) + }) + t.Run("Name is assigned", func(t *testing.T) { pipeline := &config.Pipeline{ Stages: []config.Stage{ diff --git a/pipeline/builder/types/types.go b/pipeline/builder/types/types.go index 45f704d..ebdd4e1 100644 --- a/pipeline/builder/types/types.go +++ b/pipeline/builder/types/types.go @@ -44,9 +44,13 @@ type Parameter struct { Default string `json:"default"` Required bool `json:"required"` - // TODO(bobbytables): Allow configuring parameter options - HasOptions bool `json:"hasOptions"` - Options []interface{} `json:"options"` + HasOptions bool `json:"hasOptions,omitempty"` + Options []Option `json:"options,omitempty"` +} + +// Option contains the value of the option in a given pipeline parameter +type Option struct { + Value string `json:"value,omitempty"` } // Stage is an interface to represent a Stage struct such as RunJob or Deploy @@ -75,6 +79,8 @@ type ManifestStage struct { FailPipeline *bool `json:"failPipeline,omitempty"` MarkUnstableAsSuccessful *bool `json:"markUnstableAsSuccessful,omitempty"` WaitForCompletion *bool `json:"waitForCompletion,omitempty"` + OverrideTimeout bool `json:"overrideTimeout,omitempty"` + StageTimeoutMS int64 `json:"stageTimeoutMs,omitempty"` } func (ms ManifestStage) spinnakerStage() {} diff --git a/pipeline/config/config.go b/pipeline/config/config.go index cb0283c..83fee04 100644 --- a/pipeline/config/config.go +++ b/pipeline/config/config.go @@ -43,10 +43,16 @@ type Pipeline struct { // Parameter defines a single parameter in a pipeline config type Parameter struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - Default string `yaml:"default"` - Required bool `yaml:"required"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Default string `yaml:"default"` + Required bool `yaml:"required"` + Options []Option `yaml:"options"` +} + +// Option contains the option value of a single parameter in a pipeline config +type Option struct { + Value string `yaml:"value"` } // ImageDescription contains the description of an image that can be referenced @@ -250,6 +256,7 @@ type DeployEmbeddedManifests struct { FailPipeline *bool `yaml:"failPipeline,omitempty"` MarkUnstableAsSuccessful *bool `yaml:"markUnstableAsSuccessful,omitempty"` WaitForCompletion *bool `yaml:"waitForCompletion,omitempty"` + StageTimeoutMS int64 `yaml:"stageTimeoutMs,omitempty"` } // DeleteEmbeddedManifest represents a single resource to be deleted