grim/convey

Add plan merging when extending a config file.
replace-stage
2017-10-02, Eric Fritz
09affc4055bc
Parents d436892070d8
Children 001af33c6401
Add plan merging when extending a config file.
--- a/REFERENCE.md Mon Oct 02 14:09:41 2017 -0500
+++ b/REFERENCE.md Mon Oct 02 15:37:56 2017 -0500
@@ -490,6 +490,10 @@
| ----------- | -------- | ----------- |
| 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 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.
----
--- a/examples/base.yml Mon Oct 02 14:09:41 2017 -0500
+++ b/examples/base.yml Mon Oct 02 15:37:56 2017 -0500
@@ -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 Mon Oct 02 15:37:56 2017 -0500
@@ -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 Mon Oct 02 14:09:41 2017 -0500
+++ b/loaders/convey/convey.go Mon Oct 02 15:37:56 2017 -0500
@@ -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 Mon Oct 02 14:09:41 2017 -0500
+++ b/loaders/convey/extend_test.go Mon Oct 02 15:37:56 2017 -0500
@@ -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 Mon Oct 02 15:37:56 2017 -0500
@@ -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 Mon Oct 02 14:09:41 2017 -0500
+++ b/plans/plans.go Mon Oct 02 15:37:56 2017 -0500
@@ -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 {