grim/convey

4ff734b355fd
Merged in efritz/convey/replace-stage (pull request #27)

Merge plans when extending

Approved-by: Gary Kramlich
--- a/REFERENCE.md Tue Oct 03 21:04:10 2017 -0500
+++ b/REFERENCE.md Wed Oct 04 02:32:34 2017 +0000
@@ -41,6 +41,11 @@
This example shows how to have one file extend another, inheriting all of the plans and tasks that are
not otherwise overwritten by the configuration file.
+## extends-merge.yml
+
+This example shows how to have one file extend another and specific stages of a plan can be replaced in
+the extending file.
+
## extends-slice.yml
This example shows how to expand an environment variable into a list in the context of a extended task.
@@ -490,6 +495,11 @@
| ----------- | -------- | ----------- |
| environment | | A list of environment variables to set. The should be specified in a `NAME` or `NAME=VALUE` format. If no value is provided, the value of the variable from the host will be provided if it is available. These environment variables will be applied on top of any that were set at the top-level. |
| stages | Yes | A list of `stages` to be run as part of this `plan` in the order that they should be run in. |
+| merge | | Whether or not to attempt to merge with a plan defined in the parent config file. |
+
+If a plan of the same name exists in a parent config file and merge is not set, it will replace the plan in its entirety. If merge is set, then
+the stages defined in this plan will overwrite stages of the parent plan with the same name. This behavior also applies to environments. It is an error
+to attempt to merge a stage that does not exist in the parent and new stages cannot be added as their order would not be well defined.
----
--- a/examples/base.yml Tue Oct 03 21:04:10 2017 -0500
+++ b/examples/base.yml Wed Oct 04 02:32:34 2017 +0000
@@ -21,3 +21,10 @@
plan2:
stages:
- tasks: [two]
+ plan3:
+ stages:
+ - name: pre
+ tasks: []
+ - tasks: [one, two]
+ - name: post
+ tasks: []
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/extends-merge.yml Wed Oct 04 02:32:34 2017 +0000
@@ -0,0 +1,26 @@
+# This plan extends another plan by overwriting the pre and post
+# stages of a plan defined in the parent config. This shows how
+# to build "abstract" plans which can be extended in ways that
+# allow blocks of plans to be replaced.
+
+extends: base.yml
+
+options:
+ default-plan: plan3
+
+tasks:
+ before:
+ image: gliderlabs/alpine:edge
+ command: echo "<= before"
+ after:
+ image: gliderlabs/alpine:edge
+ command: echo "=> after"
+
+plans:
+ plan3:
+ merge: true
+ stages:
+ - name: pre
+ tasks: [before]
+ - name: post
+ tasks: [after]
--- a/loaders/convey/convey.go Tue Oct 03 21:04:10 2017 -0500
+++ b/loaders/convey/convey.go Wed Oct 04 02:32:34 2017 +0000
@@ -125,8 +125,27 @@
baseConfig.Tasks[name] = task
}
+ // If the plan has the merge attribute set, try to overwrite
+ // stages of a plan that is already declared with the same name.
+ // If no such plan exists, raise an error (you're trying to
+ // overwrite a stage of nothing, which is almost certainly an error.
+
for name, plan := range cfg.Plans {
- baseConfig.Plans[name] = plan
+ if plan.Merge {
+ base, ok := baseConfig.Plans[name]
+ if !ok {
+ return nil, fmt.Errorf("cannot merge with unknown plan '%s'", name)
+ }
+
+ base, err := mergePlan(base, plan, name)
+ if err != nil {
+ return nil, err
+ }
+
+ baseConfig.Plans[name] = base
+ } else {
+ baseConfig.Plans[name] = plan
+ }
}
for name, metaPlan := range cfg.MetaPlans {
--- a/loaders/convey/extend_test.go Tue Oct 03 21:04:10 2017 -0500
+++ b/loaders/convey/extend_test.go Wed Oct 04 02:32:34 2017 +0000
@@ -180,3 +180,205 @@
Expect(cfg.Tasks["bar"].(*intrinsic.Extend).InnerTask).NotTo(BeIdenticalTo(cfg.Tasks["foo"]))
Expect(cfg.Tasks["bar"].(*intrinsic.Extend).InnerTask).To(BeEquivalentTo(cfg.Tasks["foo"]))
}
+
+func (s *conveySuite) TestReplaceStage(t sweet.T) {
+ data1 := `
+tasks:
+ pre:
+ image: pre
+ post:
+ image: post
+ foo:
+ image: foo
+plans:
+ plan1:
+ stages:
+ - name: pre
+ tasks:
+ - pre
+ - name: real-work
+ tasks:
+ - foo
+ - name: post
+ tasks:
+ - post
+ plan2:
+ stages:
+ - tasks:
+ - pre
+ - foo
+ - post
+`
+
+ data2 := `
+extends: base1.yaml
+tasks:
+ bar:
+ image: bar
+plans:
+ plan1:
+ merge: true
+ stages:
+ - name: real-work
+ tasks:
+ - bar
+ plan2:
+ stages:
+ - name: real-work
+ tasks:
+ - bar
+`
+
+ data3 := `
+extends: base2.yaml
+tasks:
+ baz:
+ image: baz
+plans:
+ plan1:
+ merge: true
+ stages:
+ - name: post
+ tasks:
+ - baz
+`
+
+ loader := &Loader{
+ fileLoader: func(name string, c *Loader) (*cConfig.Config, error) {
+ m := map[string]string{
+ "base1.yaml": data1,
+ "base2.yaml": data2,
+ }
+
+ return c.Load(".", name, []byte(m[name]))
+ },
+ }
+
+ cfg, err := loader.Load(".", ".", []byte(data3))
+ Expect(err).To(BeNil())
+ Expect(cfg.Plans).To(HaveLen(2))
+ Expect(cfg.Plans).To(HaveKey("plan1"))
+ Expect(cfg.Plans).To(HaveKey("plan2"))
+
+ Expect(cfg.Plans["plan1"].Stages).To(HaveLen(3))
+ Expect(cfg.Plans["plan1"].Stages[0].Name).To(Equal("pre"))
+ Expect(cfg.Plans["plan1"].Stages[1].Name).To(Equal("real-work"))
+ Expect(cfg.Plans["plan1"].Stages[2].Name).To(Equal("post"))
+ Expect(cfg.Plans["plan1"].Stages[0].Tasks).To(HaveLen(1))
+ Expect(cfg.Plans["plan1"].Stages[1].Tasks).To(HaveLen(1))
+ Expect(cfg.Plans["plan1"].Stages[2].Tasks).To(HaveLen(1))
+ Expect(cfg.Plans["plan1"].Stages[0].Tasks).To(ConsistOf("pre"))
+ Expect(cfg.Plans["plan1"].Stages[1].Tasks).To(ConsistOf("bar"))
+ Expect(cfg.Plans["plan1"].Stages[2].Tasks).To(ConsistOf("baz"))
+
+ Expect(cfg.Plans["plan2"].Stages).To(HaveLen(1))
+ Expect(cfg.Plans["plan2"].Stages[0].Tasks).To(HaveLen(1))
+ Expect(cfg.Plans["plan2"].Stages[0].Tasks).To(ConsistOf("bar"))
+}
+
+func (s *conveySuite) TestMergeMissingPlan(t sweet.T) {
+ data1 := `
+tasks:
+ foo:
+ image: foo
+plans:
+ plan1:
+ stages:
+ - tasks: [foo]
+`
+
+ data2 := `
+extends: base1.yaml
+tasks:
+ bar:
+ image: bar
+plans:
+ plan2:
+ merge: true
+ stages:
+ - tasks: [foo, bar]
+`
+
+ loader := &Loader{
+ fileLoader: func(name string, c *Loader) (*cConfig.Config, error) {
+ return c.Load(".", name, []byte(data1))
+ },
+ }
+
+ _, err := loader.Load(".", ".", []byte(data2))
+ Expect(err).To(MatchError("cannot merge with unknown plan 'plan2'"))
+}
+
+func (s *conveySuite) TestMergeMissingStage(t sweet.T) {
+ data1 := `
+tasks:
+ foo:
+ image: foo
+plans:
+ plan1:
+ stages:
+ - name: only
+ tasks:
+ - foo
+`
+
+ data2 := `
+extends: base1.yaml
+tasks:
+ bar:
+ image: bar
+plans:
+ plan1:
+ merge: true
+ stages:
+ - name: missing
+ tasks:
+ - bar
+`
+
+ loader := &Loader{
+ fileLoader: func(name string, c *Loader) (*cConfig.Config, error) {
+ return c.Load(".", name, []byte(data1))
+ },
+ }
+
+ _, err := loader.Load(".", ".", []byte(data2))
+ Expect(err).To(MatchError("cannot overwrite stage 'missing' in plan 'plan1' (no such stage in parent)"))
+}
+
+func (s *conveySuite) TestMergePlanEnvironment(t sweet.T) {
+ data1 := `
+tasks:
+ foo:
+ image: foo
+plans:
+ plan1:
+ stages:
+ - name: only
+ tasks:
+ - foo
+ environment:
+ - FOO=BAR
+ - BAR=BAZ
+`
+
+ data2 := `
+extends: base1.yaml
+plans:
+ plan1:
+ merge: true
+ environment:
+ - BAR=BONK
+`
+
+ loader := &Loader{
+ fileLoader: func(name string, c *Loader) (*cConfig.Config, error) {
+ return c.Load(".", name, []byte(data1))
+ },
+ }
+
+ cfg, err := loader.Load(".", ".", []byte(data2))
+ Expect(err).To(BeNil())
+ Expect(cfg.Plans["plan1"].Stages[0].Tasks).To(ConsistOf("foo"))
+ Expect(cfg.Plans["plan1"].Environment).To(ConsistOf("FOO=BAR", "BAR=BONK"))
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/loaders/convey/plans.go Wed Oct 04 02:32:34 2017 +0000
@@ -0,0 +1,38 @@
+package convey
+
+import (
+ "fmt"
+
+ "bitbucket.org/rw_grim/convey/environment"
+ "bitbucket.org/rw_grim/convey/plans"
+ "bitbucket.org/rw_grim/convey/stages"
+)
+
+// mergePlan modifies parent in-place so that any stage declared in child
+// will overwrite the stage of the same name in parent. Return an error if
+// a stage exists in child but not parent. Environments are also merged.
+func mergePlan(parent, child plans.Plan, name string) (plans.Plan, error) {
+ for _, stage := range child.Stages {
+ if !replaceStage(parent, stage) {
+ return plans.Plan{}, fmt.Errorf(
+ "cannot overwrite stage '%s' in plan '%s' (no such stage in parent)",
+ stage.Name,
+ name,
+ )
+ }
+ }
+
+ parent.Environment = environment.Merge(parent.Environment, child.Environment)
+ return parent, nil
+}
+
+func replaceStage(plan plans.Plan, stage stages.Stage) bool {
+ for i, match := range plan.Stages {
+ if match.Name == stage.Name {
+ plan.Stages[i] = stage
+ return true
+ }
+ }
+
+ return false
+}
--- a/plans/plans.go Tue Oct 03 21:04:10 2017 -0500
+++ b/plans/plans.go Wed Oct 04 02:32:34 2017 +0000
@@ -34,6 +34,7 @@
type Plan struct {
Environment yaml.StringOrSlice `yaml:"environment"`
Stages []stages.Stage `yaml:"stages"`
+ Merge bool `yaml:"merge"`
}
func (p *Plan) setup(logger *gomol.LogAdapter, st *state.State) error {