Repository: appvia/kev
Pull request #233 (2020-09-08 09:08:34)

Reconcile: overridden environment overlay settings are lost.
This resolves https://github.com/appvia/kev-planning/issues/29.

- Refactored algorithm as previous implementation was becoming awkward to patch.
- Fixed up to either rollback or keep environment overlay settings.
- Added new tests to cover new scenarios.
- Removed dependency on the `r3labs/diff` package.


Patch:
diff --git a/cmd/kev/cmd/reconcile.go b/cmd/kev/cmd/reconcile.go
index 8cb49d5..e5b6945 100644
--- a/cmd/kev/cmd/reconcile.go
+++ b/cmd/kev/cmd/reconcile.go
@@ -35,7 +35,7 @@ func runReconcileCmd(_ *cobra.Command, _ []string) error {
 		return err
 	}
 
-	os.Stdout.WriteString("\n...............................\n\n")
+	os.Stdout.WriteString("...............................\n\n")
 
 	for _, environment := range manifest.Environments {
 		filePath := path.Join(workingDir, environment.File)
diff --git a/go.mod b/go.mod
index 4fb028a..bae8447 100644
--- a/go.mod
+++ b/go.mod
@@ -13,10 +13,10 @@ require (
 	github.com/onsi/ginkgo v1.14.0
 	github.com/onsi/gomega v1.10.1
 	github.com/pkg/errors v0.9.1
-	github.com/r3labs/diff v1.1.0
 	github.com/sirupsen/logrus v1.6.0
 	github.com/spf13/cast v1.3.1
 	github.com/spf13/cobra v1.0.0
+	github.com/stretchr/testify v1.5.1 // indirect
 	github.com/x-cray/logrus-prefixed-formatter v0.5.2
 	golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
diff --git a/go.sum b/go.sum
index bb822c6..593c421 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/compose-spec/compose-go v0.0.0-20200629133725-59b25574fd55 h1:vX1uiHKgVnFBPBLxz6P28n+TKGt3jGThvDr2vkSP8D0=
-github.com/compose-spec/compose-go v0.0.0-20200629133725-59b25574fd55/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4=
 github.com/compose-spec/compose-go v0.0.0-20200907084823-057e1edc5b6f h1:YsU3/17YA/skXpCQbRcrzWJxslWZ2lmvQK0bRiCyC38=
 github.com/compose-spec/compose-go v0.0.0-20200907084823-057e1edc5b6f/go.mod h1:voTGL1mRFcKRaFbi1lXGlR1YffS/9YD1jnVl4N/rYzw=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -80,8 +78,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -99,10 +95,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
-github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
-github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -144,8 +136,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
-github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs=
@@ -191,8 +181,6 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/r3labs/diff v1.1.0 h1:V53xhrbTHrWFWq3gI4b94AjgEJOerO1+1l0xyHOBi8M=
-github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6Xig=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff --git a/pkg/kev/changeset.go b/pkg/kev/changeset.go
index 31cc7dc..7542410 100644
--- a/pkg/kev/changeset.go
+++ b/pkg/kev/changeset.go
@@ -19,24 +19,32 @@ package kev
 import (
 	"fmt"
 	"io"
-	"strconv"
+	"reflect"
 
-	"github.com/r3labs/diff"
+	"github.com/appvia/kube-devx/pkg/kev/config"
 )
 
-// newChangeset creates a changeset based on a diff.Changelog.
-// A diff.Changelog is an ordered []diff.Changes slice produced from diffing two structs.
-// E.g. Here's a diff.Change that updates ["services", 0, "labels", "kev.workload.liveness-probe-command"]:
+const (
+	CREATE = "create"
+	UPDATE = "update"
+	DELETE = "delete"
+)
+
+// newChangeset detects all changes between a destination overlay and source overlay.
+// A change is either a create, update or delete event.
+// A change targets an overlay's version, services or volumes and it's properties will depend on the actual target.
+// Example: here's a Change that creates a new service:
 // {
-//    Type: "update",   //string
-//    Path: {           // []string
-//      "services",
-//      "0",
-//      "labels",
-//      "kev.workload.liveness-probe-command"
-//    },
-//    From: "[\"CMD\", \"echo\", \"Define healthcheck command for service wordpress\"]",  // string
-//    To: "[\"CMD\", \"curl\", \"localhost:80/healthy\"]"                                 // string
+//    Type: "create",   //string
+//    Value: srcSvc,    //interface{} in this case: ServiceConfig
+// }
+// Example: here's a Change that updates a service's label:
+// {
+// 		Type:   "update",                 //string
+// 		Index:  index,                    // interface{} in this case: int
+// 		Parent: "labels",                 // string
+// 		Target: config.LabelServiceType,  // string
+// 		Value:  srcSvc.GetLabels()[config.LabelServiceType], // interface{} in this case: string
 // }
 //
 // ENV VARS NOTE:
@@ -45,236 +53,192 @@ import (
 // - A changeset will ONLY REMOVE an env var if it is removed from a project's docker-compose env vars.
 // - A changeset will NOT update or create env vars in deployment environments.
 // - To create useful diffs a project's docker-compose env vars will be taken into account.
-//
-func newChangeset(clog diff.Changelog) (changeset, error) {
-	var verChanges []change
-	volChgGroup := make(changeGroup)
-	svcChgGroup := make(changeGroup)
-
-	for _, e := range clog {
-		switch e.Path[0] {
-		case "version":
-			change := change{
-				update: e.Type == "update",
-			}
-			if e.To != nil {
-				change.value = e.To.(string)
-			}
-			verChanges = append(verChanges, change)
+func newChangeset(dst *composeOverlay, src *composeOverlay) changeset {
+	cset := changeset{}
+	detectVersionUpdate(dst, src, &cset)
+	detectServicesCreate(dst, src, &cset)
+	detectServicesDelete(dst, src, &cset)
+	detectServicesEnvironmentDelete(dst, src, &cset)
+	detectServicesUpdate(dst, src, &cset)
+	detectVolumesCreate(dst, src, &cset)
+	detectVolumesDelete(dst, src, &cset)
+	return cset
+}
+
+func detectVersionUpdate(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	if dst.Version != src.Version {
+		cset.version = change{Value: src.Version, Type: UPDATE, Target: "version"}
+	}
+}
 
-		case "services":
-			svcIndex, err := strconv.Atoi(e.Path[1])
-			if err != nil {
-				return changeset{}, err
-			}
+func detectServicesCreate(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	dstSvcSet := dst.Services.Set()
+	for _, srcSvc := range src.Services {
+		if !dstSvcSet[srcSvc.Name] {
+			cset.services = append(cset.services, change{
+				Type:  CREATE,
+				Value: srcSvc,
+			})
+		}
+	}
+}
 
-			// Do not append more changes for a service if the service is marked for deletion
-			if isServiceAlreadyMarkedForDeletion(svcChgGroup, svcIndex) {
-				continue
-			}
+func detectServicesDelete(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	srcSvcSet := src.Services.Set()
+	for index, dstSvc := range dst.Services {
+		if !srcSvcSet[dstSvc.Name] {
+			cset.services = append(cset.services, change{
+				Type:  DELETE,
+				Index: index,
+			})
+		}
+	}
+}
 
-			change := change{
-				parent: e.Path[len(e.Path)-2],
-				target: e.Path[len(e.Path)-1],
-				index:  svcIndex,
-				update: isServiceUpdateChange(e),
-				create: isServiceCreateChange(e, svcChgGroup, svcIndex),
-				delete: e.Type == "delete",
-			}
-			if e.To != nil {
-				change.value = e.To.(string)
+func detectServicesEnvironmentDelete(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	srcSvcMapping := src.Services.Map()
+	for index, dstSvc := range dst.Services {
+		srcSvc, ok := srcSvcMapping[dstSvc.Name]
+		if !ok {
+			continue
+		}
+		for envVarKey := range dstSvc.Environment {
+			if _, ok := srcSvc.Environment[envVarKey]; !ok {
+				cset.services = append(cset.services, change{
+					Type:   DELETE,
+					Index:  index,
+					Parent: "environment",
+					Target: envVarKey,
+				})
 			}
-			svcChgGroup[svcIndex] = append(svcChgGroup[svcIndex], change)
+		}
+	}
+}
 
-		case "volumes":
-			volName := e.Path[1]
+func detectServicesUpdate(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	srcSvcMapping := src.Services.Map()
+	for index, dstSvc := range dst.Services {
+		srcSvc, ok := srcSvcMapping[dstSvc.Name]
+		if !ok {
+			continue
+		}
 
-			// Do not append more changes for a volume if it's marked for deletion
-			if isVolumeAlreadyMarkedForDeletion(volChgGroup, volName) {
-				continue
-			}
+		if srcSvc.GetLabels()[config.LabelServiceType] != dstSvc.GetLabels()[config.LabelServiceType] {
+			cset.services = append(cset.services, change{
+				Type:   UPDATE,
+				Index:  index,
+				Parent: "labels",
+				Target: config.LabelServiceType,
+				Value:  srcSvc.GetLabels()[config.LabelServiceType],
+			})
+		}
+	}
+}
 
-			change := change{
-				parent: e.Path[len(e.Path)-2],
-				target: e.Path[len(e.Path)-1],
-				index:  volName,
-				update: isVolumeUpdateChange(e),
-				create: isVolumeCreateChange(e, volChgGroup, volName),
-				delete: e.Type == "delete",
-			}
-			if e.To != nil {
-				change.value = e.To.(string)
-			}
-			volChgGroup[volName] = append(volChgGroup[volName], change)
+func detectVolumesCreate(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	for srcVolKey, srcVolConfig := range src.Volumes {
+		if _, ok := dst.Volumes[srcVolKey]; !ok {
+			cset.volumes = append(cset.volumes, change{
+				Type:  CREATE,
+				Index: srcVolKey,
+				Value: srcVolConfig,
+			})
 		}
 	}
+}
 
-	return changeset{version: verChanges, services: svcChgGroup, volumes: volChgGroup}, nil
+func detectVolumesDelete(dst *composeOverlay, src *composeOverlay, cset *changeset) {
+	for dstVolKey := range dst.Volumes {
+		if _, ok := src.Volumes[dstVolKey]; !ok {
+			cset.volumes = append(cset.volumes, change{
+				Type:  DELETE,
+				Index: dstVolKey,
+			})
+		}
+	}
 }
 
 // changes returns a flat list of all available changes
 func (cset changeset) changes() []change {
 	var out []change
-	for _, vc := range cset.version {
-		out = append(out, vc)
-	}
-	for _, vc := range cset.services {
-		out = append(out, vc...)
-	}
-	for _, vc := range cset.volumes {
-		out = append(out, vc...)
+	if !reflect.DeepEqual(cset.version, change{}) {
+		out = append(out, cset.version)
 	}
+	out = append(out, cset.services...)
+	out = append(out, cset.volumes...)
 	return out
 }
 
 // HasNoPatches informs if a changeset has any patches to apply.
-// A changeset may have changes but these may not result into valid patches.
 func (cset changeset) HasNoPatches() bool {
-	for _, chg := range cset.changes() {
-		if chg.hasPatch() {
-			return false
-		}
-	}
-	return true
+	return len(cset.changes()) <= 0
 }
 
 func (cset changeset) applyVersionPatchesIfAny(o *composeOverlay, reporter io.Writer) {
-	for _, change := range cset.version {
-		if change.hasPatch() {
-			change.patchVersion(o, reporter)
-		}
+	chg := cset.version
+	if reflect.DeepEqual(chg, change{}) {
+		return
 	}
+	chg.patchVersion(o, reporter)
 }
 
 func (cset changeset) applyServicesPatchesIfAny(o *composeOverlay, reporter io.Writer) {
-	for _, group := range cset.services {
-		for _, change := range group {
-			if change.hasPatch() {
-				change.patchService(o, reporter)
-			}
-		}
+	for _, change := range cset.services {
+		change.patchService(o, reporter)
 	}
 }
 
 func (cset changeset) applyVolumesPatchesIfAny(o *composeOverlay, reporter io.Writer) {
-	for _, group := range cset.volumes {
-		for _, change := range group {
-			if change.hasPatch() {
-				change.patchVolume(o, reporter)
-			}
-		}
+	for _, change := range cset.volumes {
+		change.patchVolume(o, reporter)
 	}
 }
 
-// hasPatch informs whether a change has a valid patch to apply or not
-func (chg change) hasPatch() bool {
-	return chg.create || chg.update || chg.delete
-}
-
 func (chg change) patchVersion(overlay *composeOverlay, reporter io.Writer) {
-	if !chg.update {
+	if chg.Type != UPDATE {
 		return
 	}
 	pre := overlay.Version
-	overlay.Version = chg.value
-	_, _ = reporter.Write([]byte(fmt.Sprintf(" → version updated, from:[%s] to:[%s]\n", pre, chg.value)))
+	newValue := chg.Value.(string)
+	overlay.Version = newValue
+	_, _ = reporter.Write([]byte(fmt.Sprintf(" → version updated, from:[%s] to:[%s]\n", pre, newValue)))
 }
 
 func (chg change) patchService(overlay *composeOverlay, reporter io.Writer) {
-	if chg.create {
-		overlay.Services = append(overlay.Services, ServiceConfig{
-			Labels: map[string]string{},
-		})
-		_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s] added\n", chg.value)))
-	}
-
-	if chg.delete {
+	switch chg.Type {
+	case CREATE:
+		newValue := chg.Value.(ServiceConfig)
+		overlay.Services = append(overlay.Services, newValue)
+		_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s] added\n", newValue.Name)))
+	case DELETE:
 		switch {
-		case chg.parent == "environment":
-			delete(overlay.Services[chg.index.(int)].Environment, chg.target)
-			_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s], env var [%s] deleted\n", overlay.Services[chg.index.(int)].Name, chg.target)))
+		case chg.Parent == "environment":
+			delete(overlay.Services[chg.Index.(int)].Environment, chg.Target)
+			_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s], env var [%s] deleted\n", overlay.Services[chg.Index.(int)].Name, chg.Target)))
 		default:
-			deletedSvcName := overlay.Services[chg.index.(int)].Name
-			overlay.Services = append(overlay.Services[:chg.index.(int)], overlay.Services[chg.index.(int)+1:]...)
+			deletedSvcName := overlay.Services[chg.Index.(int)].Name
+			overlay.Services = append(overlay.Services[:chg.Index.(int)], overlay.Services[chg.Index.(int)+1:]...)
 			_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s] deleted\n", deletedSvcName)))
 		}
-	}
-
-	if chg.update {
-		switch {
-		case chg.target == "name":
-			isUpdate := len(overlay.Services[chg.index.(int)].Name) > 0
-			overlay.Services[chg.index.(int)].Name = chg.value
-			if isUpdate {
-				_, _ = reporter.Write([]byte(fmt.Sprintf(" → service name updated to: [%s]\n", chg.value)))
-			}
-		case chg.parent == "labels":
-			pre, isUpdate := overlay.Services[chg.index.(int)].Labels[chg.target]
-			overlay.Services[chg.index.(int)].Labels[chg.target] = chg.value
-			if isUpdate {
-				_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s], label [%s] updated, from:[%s] to:[%s]\n", overlay.Services[chg.index.(int)].Name, chg.target, pre, chg.value)))
+	case UPDATE:
+		if chg.Parent == "labels" {
+			pre, canUpdate := overlay.Services[chg.Index.(int)].Labels[chg.Target]
+			newValue := chg.Value.(string)
+			overlay.Services[chg.Index.(int)].Labels[chg.Target] = newValue
+			if canUpdate {
+				_, _ = reporter.Write([]byte(fmt.Sprintf(" → service [%s], label [%s] updated, from:[%s] to:[%s]\n", overlay.Services[chg.Index.(int)].Name, chg.Target, pre, newValue)))
 			}
 		}
 	}
 }
 
 func (chg change) patchVolume(overlay *composeOverlay, reporter io.Writer) {
-	if chg.create {
-		overlay.Volumes = Volumes{
-			chg.index.(string): VolumeConfig{
-				Labels: map[string]string{},
-			},
-		}
-		_, _ = reporter.Write([]byte(fmt.Sprintf(" → volume [%s] added\n", chg.index.(string))))
-	}
-
-	if chg.delete {
-		delete(overlay.Volumes, chg.index.(string))
-		_, _ = reporter.Write([]byte(fmt.Sprintf(" → volume [%s] deleted\n", chg.index.(string))))
-	}
-
-	if chg.update {
-		switch {
-		case chg.parent == "labels":
-			pre, isUpdate := overlay.Volumes[chg.index.(string)].Labels[chg.target]
-			overlay.Volumes[chg.index.(string)].Labels[chg.target] = chg.value
-			if isUpdate {
-				_, _ = reporter.Write([]byte(fmt.Sprintf(" → volume [%s], label [%s] updated, from:[%s] to:[%s]\n", chg.index.(string), chg.target, pre, chg.value)))
-			}
-		}
+	switch chg.Type {
+	case CREATE:
+		overlay.Volumes[chg.Index.(string)] = chg.Value.(VolumeConfig)
+		_, _ = reporter.Write([]byte(fmt.Sprintf(" → volume [%s] added\n", chg.Index.(string))))
+	case DELETE:
+		delete(overlay.Volumes, chg.Index.(string))
+		_, _ = reporter.Write([]byte(fmt.Sprintf(" → volume [%s] deleted\n", chg.Index.(string))))
 	}
 }
-
-func isServiceAlreadyMarkedForDeletion(chgGroup changeGroup, index int) bool {
-	group, ok := chgGroup[index]
-	return ok == true && group[0].delete && group[0].target == "name"
-}
-
-func isServiceCreateChange(e diff.Change, chgGroup changeGroup, index int) bool {
-	_, ok := chgGroup[index]
-	parent := e.Path[len(e.Path)-2]
-
-	// environment is a special case, see ENV VARS NOTE
-	return e.Type == "create" && ok == false && parent != "environment"
-}
-
-func isServiceUpdateChange(e diff.Change) bool {
-	parent := e.Path[len(e.Path)-2]
-
-	// environment is a special case, see ENV VARS NOTE
-	return (e.Type == "create" || e.Type == "update") && parent != "environment"
-}
-
-func isVolumeAlreadyMarkedForDeletion(chgGroup changeGroup, key string) bool {
-	group, ok := chgGroup[key]
-	return ok == true && group[0].delete
-}
-
-func isVolumeCreateChange(e diff.Change, chgGroup changeGroup, key string) bool {
-	_, ok := chgGroup[key]
-	return e.Type == "create" && ok == false
-}
-
-func isVolumeUpdateChange(e diff.Change) bool {
-	return e.Type == "create" || e.Type == "update"
-}
diff --git a/pkg/kev/envs.go b/pkg/kev/envs.go
index 935f05a..b059de2 100644
--- a/pkg/kev/envs.go
+++ b/pkg/kev/envs.go
@@ -156,11 +156,7 @@ func (e *Environment) loadOverlay() (*Environment, error) {
 func (e *Environment) reconcile(overlay *composeOverlay, reporter io.Writer) error {
 	_, _ = reporter.Write([]byte(fmt.Sprintf("✓ Reconciling environment [%s]\n", e.Name)))
 
-	cset, err := overlay.diff(e.overlay)
-	if err != nil {
-		return err
-	}
-
+	cset := overlay.diff(e.overlay)
 	if cset.HasNoPatches() {
 		_, _ = reporter.Write([]byte(fmt.Sprint(" → nothing to update\n")))
 		return nil
diff --git a/pkg/kev/extract.go b/pkg/kev/extract.go
index 4e21221..5e34721 100644
--- a/pkg/kev/extract.go
+++ b/pkg/kev/extract.go
@@ -60,13 +60,13 @@ func extractVolumesLabels(c *ComposeProject, out *composeOverlay) {
 	for _, v := range c.VolumeNames() {
 		labels := map[string]string{}
 
-		if storageClass, ok := vols[v].Labels[config.LabelVolumeStorageClass]; ok {
+		if storageClass, ok := c.Volumes[v].Labels[config.LabelVolumeStorageClass]; ok {
 			labels[config.LabelVolumeStorageClass] = storageClass
 		} else {
 			labels[config.LabelVolumeStorageClass] = config.DefaultVolumeClass
 		}
 
-		if volSize, ok := vols[v].Labels[config.LabelVolumeSize]; ok {
+		if volSize, ok := c.Volumes[v].Labels[config.LabelVolumeSize]; ok {
 			labels[config.LabelVolumeSize] = volSize
 		} else {
 			labels[config.LabelVolumeSize] = config.DefaultVolumeSize
diff --git a/pkg/kev/manifest.go b/pkg/kev/manifest.go
index ea12b6c..d655491 100644
--- a/pkg/kev/manifest.go
+++ b/pkg/kev/manifest.go
@@ -69,13 +69,13 @@ func (m *Manifest) GetEnvironment(name string) (*Environment, error) {
 // GetEnvironments returns filtered app environments.
 // If no filter is provided all app environments will be returned.
 func (m *Manifest) GetEnvironments(filter []string) (Environments, error) {
-	var out = make([]*Environment, len(m.Environments))
-
 	if len(filter) == 0 {
-		copy(out, m.Environments)
-		return out, nil
+		var allOut = make([]*Environment, len(m.Environments))
+		copy(allOut, m.Environments)
+		return allOut, nil
 	}
 
+	var out Environments
 	for _, f := range filter {
 		e, err := m.GetEnvironment(f)
 		if err != nil {
diff --git a/pkg/kev/overlay.go b/pkg/kev/overlay.go
index 96864e8..3c47c60 100644
--- a/pkg/kev/overlay.go
+++ b/pkg/kev/overlay.go
@@ -24,7 +24,6 @@ import (
 	composego "github.com/compose-spec/compose-go/types"
 	"github.com/imdario/mergo"
 	"github.com/pkg/errors"
-	"github.com/r3labs/diff"
 )
 
 // MarshalYAML makes Services implement yaml.Marshaller.
@@ -45,19 +44,32 @@ func (s Services) MarshalJSON() ([]byte, error) {
 	return json.MarshalIndent(data, "", "  ")
 }
 
+// Map converts services to a map.
+func (s Services) Map() map[string]ServiceConfig {
+	out := map[string]ServiceConfig{}
+	for _, service := range s {
+		out[service.Name] = service
+	}
+	return out
+}
+
+// Set converts services to a set.
+func (s Services) Set() map[string]bool {
+	out := map[string]bool{}
+	for _, service := range s {
+		out[service.Name] = true
+	}
+	return out
+}
+
 // GetLabels gets a service's labels
 func (sc ServiceConfig) GetLabels() map[string]string {
 	return sc.Labels
 }
 
 // diff detects changes between an overlay against another overlay.
-func (o *composeOverlay) diff(other *composeOverlay) (changeset, error) {
-	d, _ := diff.NewDiffer()
-	clog, err := d.Diff(other, o)
-	if err != nil {
-		return changeset{}, err
-	}
-	return newChangeset(clog)
+func (o *composeOverlay) diff(other *composeOverlay) changeset {
+	return newChangeset(other, o)
 }
 
 // patch patches an overlay based on the supplied changeset patches.
diff --git a/pkg/kev/types.go b/pkg/kev/types.go
index 2bebe7b..06815fd 100644
--- a/pkg/kev/types.go
+++ b/pkg/kev/types.go
@@ -78,20 +78,17 @@ type VolumeConfig struct {
 
 // changeset tracks changes made to a version, services and volumes
 type changeset struct {
-	version  []change
-	services changeGroup
-	volumes  changeGroup
+	version  change
+	services []change
+	volumes  []change
 }
 
-// changeGroup groups related changes
-type changeGroup map[interface{}][]change
-
 // change describes a create, update or delete modification
 // targeting an attribute in a version, service or volume.
 type change struct {
-	parent                 string      // the parent compose attribute for the modification (if available), e.g. labels
-	target                 string      // the target for the modification, e.g. kev.workload.liveness-probe-command
-	value                  string      // the new value, e.g. "[\"CMD\", \"curl\", \"localhost:80/healthy\"]"
-	index                  interface{} // the index in the changeset's services or volumes
-	update, create, delete bool        // the target operation - create and delete happen at a service/volume level
+	Type   string
+	Value  interface{}
+	Parent string
+	Target string
+	Index  interface{}
 }
diff --git a/vendor/github.com/r3labs/diff/.gitignore b/vendor/github.com/r3labs/diff/.gitignore
deleted file mode 100644
index a1338d6..0000000
--- a/vendor/github.com/r3labs/diff/.gitignore
+++ /dev/null
@@ -1,14 +0,0 @@
-# Binaries for programs and plugins
-*.exe
-*.dll
-*.so
-*.dylib
-
-# Test binary, build with `go test -c`
-*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
-# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
-.glide/
diff --git a/vendor/github.com/r3labs/diff/CONTRIBUTING.md b/vendor/github.com/r3labs/diff/CONTRIBUTING.md
deleted file mode 100644
index 3c00b27..0000000
--- a/vendor/github.com/r3labs/diff/CONTRIBUTING.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# Contributing guidelines
-
-Looking to contribute something to this project? Here's how you can help:
-
-Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved.
-
-Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features.
-
-We also have a [code of conduct](https://ernest.io/conduct).
-
-## Using the issue tracker
-
-The issue tracker is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests), but please respect the following restrictions:
-
-* Please **do not** use the issue tracker for personal support requests.
-
-* Please **do not** derail issues. Keep the discussion on topic and
-  respect the opinions of others.
-
-<a name="bugs"></a>
-## Bug reports
-
-A bug is a _demonstrable problem_ that is caused by the code in the repository.
-Good bug reports are extremely helpful - thank you!
-
-Guidelines for bug reports:
-
-1. **Use the GitHub issue search** &mdash; check if the issue has already been
-   reported.
-
-2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
-   latest `master` or `develop` branch in the repository.
-
-3. **Isolate the problem** &mdash; create a reduced test case and a live example.
-
-A good bug report shouldn't leave others needing to chase you up for more
-information. Please try to be as detailed as possible in your report. What is
-your environment? What steps will reproduce the issue? Which environment experience the problem? What would you expect to be the outcome? All these
-details will help people to fix any potential bugs.
-
-Example:
-
-> Short and descriptive example bug report title
->
-> A summary of the issue and the environment in which it occurs. If
-> suitable, include the steps required to reproduce the bug.
->
-> 1. This is the first step
-> 2. This is the second step
-> 3. Further steps, etc.
->
-> `<url>` - a link to the reduced test case
->
-> Any other information you want to share that is relevant to the issue being
-> reported. This might include the lines of code that you have identified as
-> causing the bug, and potential solutions (and your opinions on their
-> merits).
-
-<a name="features"></a>
-## Feature requests
-
-Feature requests are welcome. But take a moment to find out whether your idea
-fits with the scope and aims of the project. It's up to *you* to make a strong
-case to convince the project's developers of the merits of this feature. Please
-provide as much detail and context as possible.
-
-<a name="pull-requests"></a>
-## Pull requests
-
-Good pull requests - patches, improvements, new features - are a fantastic
-help. They should remain focused in scope and avoid containing unrelated
-commits.
-
-[**Please ask first**](https://ernest.io/community) before embarking on any significant pull request (e.g.
-implementing features, refactoring code, porting to a different language),
-otherwise you risk spending a lot of time working on something that the
-project's developers might not want to merge into the project.
-
-Please adhere to the coding conventions used throughout a project (indentation,
-accurate comments, etc.) and any other requirements (such as test coverage).
diff --git a/vendor/github.com/r3labs/diff/LICENSE b/vendor/github.com/r3labs/diff/LICENSE
deleted file mode 100644
index a612ad9..0000000
--- a/vendor/github.com/r3labs/diff/LICENSE
+++ /dev/null
@@ -1,373 +0,0 @@
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
-    means each individual or legal entity that creates, contributes to
-    the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
-    means the combination of the Contributions of others (if any) used
-    by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
-    means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
-    means Source Code Form to which the initial Contributor has attached
-    the notice in Exhibit A, the Executable Form of such Source Code
-    Form, and Modifications of such Source Code Form, in each case
-    including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
-    means
-
-    (a) that the initial Contributor has attached the notice described
-        in Exhibit B to the Covered Software; or
-
-    (b) that the Covered Software was made available under the terms of
-        version 1.1 or earlier of the License, but not also under the
-        terms of a Secondary License.
-
-1.6. "Executable Form"
-    means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
-    means a work that combines Covered Software with other material, in
-    a separate file or files, that is not Covered Software.
-
-1.8. "License"
-    means this document.
-
-1.9. "Licensable"
-    means having the right to grant, to the maximum extent possible,
-    whether at the time of the initial grant or subsequently, any and
-    all of the rights conveyed by this License.
-
-1.10. "Modifications"
-    means any of the following:
-
-    (a) any file in Source Code Form that results from an addition to,
-        deletion from, or modification of the contents of Covered
-        Software; or
-
-    (b) any new file in Source Code Form that contains any Covered
-        Software.
-
-1.11. "Patent Claims" of a Contributor
-    means any patent claim(s), including without limitation, method,
-    process, and apparatus claims, in any patent Licensable by such
-    Contributor that would be infringed, but for the grant of the
-    License, by the making, using, selling, offering for sale, having
-    made, import, or transfer of either its Contributions or its
-    Contributor Version.
-
-1.12. "Secondary License"
-    means either the GNU General Public License, Version 2.0, the GNU
-    Lesser General Public License, Version 2.1, the GNU Affero General
-    Public License, Version 3.0, or any later versions of those
-    licenses.
-
-1.13. "Source Code Form"
-    means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
-    means an individual or a legal entity exercising rights under this
-    License. For legal entities, "You" includes any entity that
-    controls, is controlled by, or is under common control with You. For
-    purposes of this definition, "control" means (a) the power, direct
-    or indirect, to cause the direction or management of such entity,
-    whether by contract or otherwise, or (b) ownership of more than
-    fifty percent (50%) of the outstanding shares or beneficial
-    ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
-    Licensable by such Contributor to use, reproduce, make available,
-    modify, display, perform, distribute, and otherwise exploit its
-    Contributions, either on an unmodified basis, with Modifications, or
-    as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
-    for sale, have made, import, and otherwise transfer either its
-    Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
-    or
-
-(b) for infringements caused by: (i) Your and any other third party's
-    modifications of Covered Software, or (ii) the combination of its
-    Contributions with other software (except as part of its Contributor
-    Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
-    its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
-    Form, as described in Section 3.1, and You must inform recipients of
-    the Executable Form how they can obtain a copy of such Source Code
-    Form by reasonable means in a timely manner, at a charge no more
-    than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
-    License, or sublicense it under different terms, provided that the
-    license for the Executable Form does not attempt to limit or alter
-    the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-*                                                                      *
-*  6. Disclaimer of Warranty                                           *
-*  -------------------------                                           *
-*                                                                      *
-*  Covered Software is provided under this License on an "as is"       *
-*  basis, without warranty of any kind, either expressed, implied, or  *
-*  statutory, including, without limitation, warranties that the       *
-*  Covered Software is free of defects, merchantable, fit for a        *
-*  particular purpose or non-infringing. The entire risk as to the     *
-*  quality and performance of the Covered Software is with You.        *
-*  Should any Covered Software prove defective in any respect, You     *
-*  (not any Contributor) assume the cost of any necessary servicing,   *
-*  repair, or correction. This disclaimer of warranty constitutes an   *
-*  essential part of this License. No use of any Covered Software is   *
-*  authorized under this License except under this disclaimer.         *
-*                                                                      *
-************************************************************************
-
-************************************************************************
-*                                                                      *
-*  7. Limitation of Liability                                          *
-*  --------------------------                                          *
-*                                                                      *
-*  Under no circumstances and under no legal theory, whether tort      *
-*  (including negligence), contract, or otherwise, shall any           *
-*  Contributor, or anyone who distributes Covered Software as          *
-*  permitted above, be liable to You for any direct, indirect,         *
-*  special, incidental, or consequential damages of any character      *
-*  including, without limitation, damages for lost profits, loss of    *
-*  goodwill, work stoppage, computer failure or malfunction, or any    *
-*  and all other commercial damages or losses, even if such party      *
-*  shall have been informed of the possibility of such damages. This   *
-*  limitation of liability shall not apply to liability for death or   *
-*  personal injury resulting from such party's negligence to the       *
-*  extent applicable law prohibits such limitation. Some               *
-*  jurisdictions do not allow the exclusion or limitation of           *
-*  incidental or consequential damages, so this exclusion and          *
-*  limitation may not apply to You.                                    *
-*                                                                      *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
-  This Source Code Form is subject to the terms of the Mozilla Public
-  License, v. 2.0. If a copy of the MPL was not distributed with this
-  file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
-  This Source Code Form is "Incompatible With Secondary Licenses", as
-  defined by the Mozilla Public License, v. 2.0.
diff --git a/vendor/github.com/r3labs/diff/Makefile b/vendor/github.com/r3labs/diff/Makefile
deleted file mode 100644
index 0b1c346..0000000
--- a/vendor/github.com/r3labs/diff/Makefile
+++ /dev/null
@@ -1,8 +0,0 @@
-install:
-	go install -v ${LDFLAGS}
-
-test:
-	@go test -v -cover ./...
-
-cover:
-	@go test -coverprofile cover.out
diff --git a/vendor/github.com/r3labs/diff/README.md b/vendor/github.com/r3labs/diff/README.md
deleted file mode 100644
index 0248536..0000000
--- a/vendor/github.com/r3labs/diff/README.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# diff
-
-A library for diffing golang structures and values.
-
-Utilizing field tags and reflection, it is able to compare two structures of the same type and create a changelog of all modified values. The produced changelog can easily be serialized to json.
-
-## Build status
-
-* Master [![CircleCI](https://circleci.com/gh/r3labs/diff/tree/master.svg?style=svg)](https://circleci.com/gh/r3labs/diff/tree/master)
-
-## Installation
-
-```
-go get github.com/r3labs/diff
-```
-
-## Changelog Format
-
-When diffing two structures using `Diff`, a changelog will be produced. Any detected changes will populate the changelog array with a Change type:
-
-```go
-type Change struct {
-	Type string      // The type of change detected; can be one of create, update or delete
-	Path []string    // The path of the detected change; will contain any field name or array index that was part of the traversal
-	From interface{} // The original value that was present in the "from" structure
-	To   interface{} // The new value that was detected as a change in the "to" structure
-}
-```
-
-Given the example below, we are diffing two slices where the third element has been removed:
-
-```go
-from := []int{1, 2, 3, 4}
-to := []int{1, 2, 4}
-
-changelog, _ := diff.Diff(from, to)
-```
-
-The resultant changelog should contain one change:
-
-```go
-Change{
-    Type: "delete",
-    Path: ["2"],
-    From: 3,
-    To:   nil,
-}
-```
-
-## Supported Types
-
-A diffable value can be/contain any of the following types:
-
-* struct
-* slice
-* string
-* int
-* bool
-* map
-* pointer
-
-### Tags
-
-In order for struct fields to be compared, they must be tagged with a given name. All tag values are prefixed with `diff`. i.e. `diff:"items"`.
-
-* `-` : In the event that you want to exclude a value from the diff, you can use the tag `diff:"-"` and the field will be ignored.
-
-* `identifier` : If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name,identifier"`
-
-* `immutable` : Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values.
-
-## Usage
-
-### Basic Example
-
-Diffing a basic set of values can be accomplished using the diff functions. Any items that specify a "diff" tag using a name will be compared.
-
-```go
-import "github.com/r3labs/diff"
-
-type Order struct {
-    ID    string `diff:"id"`
-    Items []int  `diff:"items"`
-}
-
-func main() {
-    a := Order{
-        ID: "1234",
-        Items: []int{1, 2, 3, 4},
-    }
-
-    b := Order{
-        ID: "1234",
-        Items: []int{1, 2, 4},
-    }
-
-    changelog, err := diff.Diff(a, b)
-    ...
-}
-```
-
-In this example, the output generated in the changelog will indicate that the third element with a value of '3' was removed from items.
-When marshalling the changelog to json, the output will look like:
-
-```json
-[
-    {
-        "type": "delete",
-        "path": ["items", "2"],
-        "from": 3,
-        "to": null
-    }
-]
-```
-
-### Options and Configuration
-
-You can create a new instance of a differ that allows options to be set.
-
-```go
-import "github.com/r3labs/diff"
-
-type Order struct {
-    ID    string `diff:"id"`
-    Items []int  `diff:"items"`
-}
-
-func main() {
-    a := Order{
-        ID: "1234",
-        Items: []int{1, 2, 3, 4},
-    }
-
-    b := Order{
-        ID: "1234",
-        Items: []int{1, 2, 4},
-    }
-
-	d, err := diff.NewDiffer(diff.SliceOrdering(true))
-	if err != nil {
-		panic(err)
-	}
-
-    changelog, err := d.Diff(a, b)
-    ...
-}
-```
-
-Supported options are:
-
-`SliceOrdering` ensures that the ordering of items in a slice is taken into account
-
-
-## Running Tests
-
-```
-make test
-```
-
-## Contributing
-
-Please read through our
-[contributing guidelines](CONTRIBUTING.md).
-Included are directions for opening issues, coding standards, and notes on
-development.
-
-Moreover, if your pull request contains patches or features, you must include
-relevant unit tests.
-
-## Versioning
-
-For transparency into our release cycle and in striving to maintain backward
-compatibility, this project is maintained under [the Semantic Versioning guidelines](http://semver.org/).
-
-## Copyright and License
-
-Code and documentation copyright since 2015 r3labs.io authors.
-
-Code released under
-[the Mozilla Public License Version 2.0](LICENSE).
diff --git a/vendor/github.com/r3labs/diff/comparative.go b/vendor/github.com/r3labs/diff/comparative.go
deleted file mode 100644
index f92ff6b..0000000
--- a/vendor/github.com/r3labs/diff/comparative.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-// Comparative ...
-type Comparative struct {
-	A, B *reflect.Value
-}
-
-// ComparativeList : stores indexed comparative
-type ComparativeList struct {
-	m    map[interface{}]*Comparative
-	keys []interface{}
-}
-
-// NewComparativeList : returns a new comparative list
-func NewComparativeList() *ComparativeList {
-	return &ComparativeList{
-		m:    make(map[interface{}]*Comparative),
-		keys: make([]interface{}, 0),
-	}
-}
-
-func (cl *ComparativeList) addA(k interface{}, v *reflect.Value) {
-	if (*cl).m[k] == nil {
-		(*cl).m[k] = &Comparative{}
-		(*cl).keys = append((*cl).keys, k)
-	}
-	(*cl).m[k].A = v
-}
-
-func (cl *ComparativeList) addB(k interface{}, v *reflect.Value) {
-	if (*cl).m[k] == nil {
-		(*cl).m[k] = &Comparative{}
-		(*cl).keys = append((*cl).keys, k)
-	}
-	(*cl).m[k].B = v
-}
diff --git a/vendor/github.com/r3labs/diff/diff.go b/vendor/github.com/r3labs/diff/diff.go
deleted file mode 100644
index 4c34bba..0000000
--- a/vendor/github.com/r3labs/diff/diff.go
+++ /dev/null
@@ -1,285 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"errors"
-	"fmt"
-	"reflect"
-	"strconv"
-	"strings"
-)
-
-var (
-	// ErrTypeMismatch Compared types do not match
-	ErrTypeMismatch = errors.New("types do not match")
-	// ErrInvalidChangeType The specified change values are not unsupported
-	ErrInvalidChangeType = errors.New("change type must be one of 'create' or 'delete'")
-)
-
-const (
-	// CREATE represents when an element has been added
-	CREATE = "create"
-	// UPDATE represents when an element has been updated
-	UPDATE = "update"
-	// DELETE represents when an element has been removed
-	DELETE = "delete"
-)
-
-// Differ a configurable diff instance
-type Differ struct {
-	SliceOrdering       bool
-	DisableStructValues bool
-	customValueDiffers  []ValueDiffer
-	cl                  Changelog
-}
-
-// Changelog stores a list of changed items
-type Changelog []Change
-
-// Change stores information about a changed item
-type Change struct {
-	Type string      `json:"type"`
-	Path []string    `json:"path"`
-	From interface{} `json:"from"`
-	To   interface{} `json:"to"`
-}
-
-// ValueDiffer is an interface for custom differs
-type ValueDiffer interface {
-	Match(a, b reflect.Value) bool
-	Diff(cl *Changelog, path []string, a, b reflect.Value) error
-}
-
-// Changed returns true if both values differ
-func Changed(a, b interface{}) bool {
-	cl, _ := Diff(a, b)
-	return len(cl) > 0
-}
-
-// Diff returns a changelog of all mutated values from both
-func Diff(a, b interface{}) (Changelog, error) {
-	var d Differ
-
-	return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b))
-}
-
-// NewDiffer creates a new configurable diffing object
-func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) {
-	var d Differ
-
-	for _, opt := range opts {
-		err := opt(&d)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return &d, nil
-}
-
-// StructValues gets all values from a struct
-// values are stored as "created" or "deleted" entries in the changelog,
-// depending on the change type specified
-func StructValues(t string, path []string, s interface{}) (Changelog, error) {
-	var d Differ
-	v := reflect.ValueOf(s)
-
-	return d.cl, d.structValues(t, path, v)
-}
-
-// Filter filter changes based on path. Paths may contain valid regexp to match items
-func (cl *Changelog) Filter(path []string) Changelog {
-	var ncl Changelog
-
-	for _, c := range *cl {
-		if pathmatch(path, c.Path) {
-			ncl = append(ncl, c)
-		}
-	}
-
-	return ncl
-}
-
-// Diff returns a changelog of all mutated values from both
-func (d *Differ) Diff(a, b interface{}) (Changelog, error) {
-	return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b))
-}
-
-func (d *Differ) diff(path []string, a, b reflect.Value) error {
-	// check if types match or are
-	if invalid(a, b) {
-		return ErrTypeMismatch
-	}
-
-	// first go through custom diff functions
-	if len(d.customValueDiffers) > 0 {
-		for _, vd := range d.customValueDiffers {
-			if vd.Match(a, b) {
-				err := vd.Diff(&d.cl, path, a, b)
-				if err != nil {
-					return err
-				}
-				return nil
-			}
-		}
-	}
-
-	// then built-in diff functions
-	switch {
-	case are(a, b, reflect.Struct, reflect.Invalid):
-		return d.diffStruct(path, a, b)
-	case are(a, b, reflect.Slice, reflect.Invalid):
-		return d.diffSlice(path, a, b)
-	case are(a, b, reflect.String, reflect.Invalid):
-		return d.diffString(path, a, b)
-	case are(a, b, reflect.Bool, reflect.Invalid):
-		return d.diffBool(path, a, b)
-	case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid):
-		return d.diffInt(path, a, b)
-	case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid):
-		return d.diffUint(path, a, b)
-	case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid):
-		return d.diffFloat(path, a, b)
-	case are(a, b, reflect.Map, reflect.Invalid):
-		return d.diffMap(path, a, b)
-	case are(a, b, reflect.Ptr, reflect.Invalid):
-		return d.diffPtr(path, a, b)
-	case are(a, b, reflect.Interface, reflect.Invalid):
-		return d.diffInterface(path, a, b)
-	default:
-		return errors.New("unsupported type: " + a.Kind().String())
-	}
-}
-
-func (cl *Changelog) Add(t string, path []string, from, to interface{}) {
-	(*cl) = append((*cl), Change{
-		Type: t,
-		Path: path,
-		From: from,
-		To:   to,
-	})
-}
-
-func tagName(f reflect.StructField) string {
-	t := f.Tag.Get("diff")
-
-	parts := strings.Split(t, ",")
-	if len(parts) < 1 {
-		return "-"
-	}
-
-	return parts[0]
-}
-
-func identifier(v reflect.Value) interface{} {
-	for i := 0; i < v.NumField(); i++ {
-		if hasTagOption(v.Type().Field(i), "identifier") {
-			return v.Field(i).Interface()
-		}
-	}
-
-	return nil
-}
-
-func hasTagOption(f reflect.StructField, opt string) bool {
-	parts := strings.Split(f.Tag.Get("diff"), ",")
-	if len(parts) < 2 {
-		return false
-	}
-
-	for _, option := range parts[1:] {
-		if option == opt {
-			return true
-		}
-	}
-
-	return false
-}
-
-func swapChange(t string, c Change) Change {
-	nc := Change{
-		Type: t,
-		Path: c.Path,
-	}
-
-	switch t {
-	case CREATE:
-		nc.To = c.To
-	case DELETE:
-		nc.From = c.To
-	}
-
-	return nc
-}
-
-func idstring(v interface{}) string {
-	switch v.(type) {
-	case string:
-		return v.(string)
-	case int:
-		return strconv.Itoa(v.(int))
-	default:
-		return fmt.Sprint(v)
-	}
-}
-
-func invalid(a, b reflect.Value) bool {
-	if a.Kind() == b.Kind() {
-		return false
-	}
-
-	if a.Kind() == reflect.Invalid {
-		return false
-	}
-	if b.Kind() == reflect.Invalid {
-		return false
-	}
-
-	return true
-}
-
-func are(a, b reflect.Value, kinds ...reflect.Kind) bool {
-	var amatch, bmatch bool
-
-	for _, k := range kinds {
-		if a.Kind() == k {
-			amatch = true
-		}
-		if b.Kind() == k {
-			bmatch = true
-		}
-	}
-
-	return amatch && bmatch
-}
-
-func AreType(a, b reflect.Value, types ...reflect.Type) bool {
-	var amatch, bmatch bool
-
-	for _, t := range types {
-		if a.Kind() != reflect.Invalid {
-			if a.Type() == t {
-				amatch = true
-			}
-		}
-		if b.Kind() != reflect.Invalid {
-			if b.Type() == t {
-				bmatch = true
-			}
-		}
-	}
-
-	return amatch && bmatch
-}
-
-func copyAppend(src []string, elems ...string) []string {
-	dst := make([]string, len(src)+len(elems))
-	copy(dst, src)
-	for i := len(src); i < len(src)+len(elems); i++ {
-		dst[i] = elems[i-len(src)]
-	}
-	return dst
-}
diff --git a/vendor/github.com/r3labs/diff/diff_bool.go b/vendor/github.com/r3labs/diff/diff_bool.go
deleted file mode 100644
index 9aad631..0000000
--- a/vendor/github.com/r3labs/diff/diff_bool.go
+++ /dev/null
@@ -1,29 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import "reflect"
-
-func (d *Differ) diffBool(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if a.Bool() != b.Bool() {
-		d.cl.Add(UPDATE, path, a.Interface(), b.Interface())
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_comparative.go b/vendor/github.com/r3labs/diff/diff_comparative.go
deleted file mode 100644
index 83cad41..0000000
--- a/vendor/github.com/r3labs/diff/diff_comparative.go
+++ /dev/null
@@ -1,58 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-func (d *Differ) diffComparative(path []string, c *ComparativeList) error {
-	for _, k := range c.keys {
-		fpath := copyAppend(path, idstring(k))
-
-		nv := reflect.ValueOf(nil)
-
-		if c.m[k].A == nil {
-			c.m[k].A = &nv
-		}
-
-		if c.m[k].B == nil {
-			c.m[k].B = &nv
-		}
-
-		err := d.diff(fpath, *c.m[k].A, *c.m[k].B)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func comparative(a, b reflect.Value) bool {
-	if a.Len() > 0 {
-		ae := a.Index(0)
-		ak := getFinalValue(ae)
-
-		if ak.Kind() == reflect.Struct {
-			if identifier(ak) != nil {
-				return true
-			}
-		}
-	}
-
-	if b.Len() > 0 {
-		be := b.Index(0)
-		bk := getFinalValue(be)
-
-		if bk.Kind() == reflect.Struct {
-			if identifier(bk) != nil {
-				return true
-			}
-		}
-	}
-
-	return false
-}
diff --git a/vendor/github.com/r3labs/diff/diff_float.go b/vendor/github.com/r3labs/diff/diff_float.go
deleted file mode 100644
index 2746dc0..0000000
--- a/vendor/github.com/r3labs/diff/diff_float.go
+++ /dev/null
@@ -1,35 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-func (d *Differ) diffFloat(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if a.Float() != b.Float() {
-		if a.CanInterface() {
-			d.cl.Add(UPDATE, path, a.Interface(), b.Interface())
-		} else {
-			d.cl.Add(UPDATE, path, a.Float(), b.Float())
-		}
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_int.go b/vendor/github.com/r3labs/diff/diff_int.go
deleted file mode 100644
index 03158b5..0000000
--- a/vendor/github.com/r3labs/diff/diff_int.go
+++ /dev/null
@@ -1,35 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-func (d *Differ) diffInt(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if a.Int() != b.Int() {
-		if a.CanInterface() {
-			d.cl.Add(UPDATE, path, a.Interface(), b.Interface())
-		} else {
-			d.cl.Add(UPDATE, path, a.Int(), b.Int())
-		}
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_interface.go b/vendor/github.com/r3labs/diff/diff_interface.go
deleted file mode 100644
index d4161c6..0000000
--- a/vendor/github.com/r3labs/diff/diff_interface.go
+++ /dev/null
@@ -1,39 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import "reflect"
-
-func (d *Differ) diffInterface(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if a.IsNil() && b.IsNil() {
-		return nil
-	}
-
-	if a.IsNil() {
-		d.cl.Add(UPDATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.IsNil() {
-		d.cl.Add(UPDATE, path, a.Interface(), nil)
-		return nil
-	}
-
-	return d.diff(path, a.Elem(), b.Elem())
-}
diff --git a/vendor/github.com/r3labs/diff/diff_map.go b/vendor/github.com/r3labs/diff/diff_map.go
deleted file mode 100644
index 815cbeb..0000000
--- a/vendor/github.com/r3labs/diff/diff_map.go
+++ /dev/null
@@ -1,69 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"fmt"
-	"reflect"
-)
-
-func (d *Differ) diffMap(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		return d.mapValues(CREATE, path, b)
-	}
-
-	if b.Kind() == reflect.Invalid {
-		return d.mapValues(DELETE, path, a)
-	}
-
-	c := NewComparativeList()
-
-	for _, k := range a.MapKeys() {
-		ae := a.MapIndex(k)
-		c.addA(k.Interface(), &ae)
-	}
-
-	for _, k := range b.MapKeys() {
-		be := b.MapIndex(k)
-		c.addB(k.Interface(), &be)
-	}
-
-	return d.diffComparative(path, c)
-}
-
-func (d *Differ) mapValues(t string, path []string, a reflect.Value) error {
-	if t != CREATE && t != DELETE {
-		return ErrInvalidChangeType
-	}
-
-	if a.Kind() == reflect.Ptr {
-		a = reflect.Indirect(a)
-	}
-
-	if a.Kind() != reflect.Map {
-		return ErrTypeMismatch
-	}
-
-	x := reflect.New(a.Type()).Elem()
-
-	for _, k := range a.MapKeys() {
-		ae := a.MapIndex(k)
-		xe := x.MapIndex(k)
-
-		err := d.diff(append(path, fmt.Sprint(k.Interface())), xe, ae)
-		if err != nil {
-			return err
-		}
-	}
-
-	for i := 0; i < len(d.cl); i++ {
-		// only swap changes on the relevant map
-		if pathmatch(path, d.cl[i].Path) {
-			d.cl[i] = swapChange(t, d.cl[i])
-		}
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_pointer.go b/vendor/github.com/r3labs/diff/diff_pointer.go
deleted file mode 100644
index 5b14b8a..0000000
--- a/vendor/github.com/r3labs/diff/diff_pointer.go
+++ /dev/null
@@ -1,43 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-func (d *Differ) diffPtr(path []string, a, b reflect.Value) error {
-	if a.Kind() != b.Kind() {
-		if a.Kind() == reflect.Invalid {
-			if !b.IsNil() {
-				return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b))
-			}
-		}
-
-		if b.Kind() == reflect.Invalid {
-			if !a.IsNil() {
-				return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil))
-			}
-		}
-
-		return ErrTypeMismatch
-	}
-
-	if a.IsNil() && b.IsNil() {
-		return nil
-	}
-
-	if a.IsNil() {
-		d.cl.Add(UPDATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.IsNil() {
-		d.cl.Add(UPDATE, path, a.Interface(), nil)
-		return nil
-	}
-
-	return d.diff(path, reflect.Indirect(a), reflect.Indirect(b))
-}
diff --git a/vendor/github.com/r3labs/diff/diff_slice.go b/vendor/github.com/r3labs/diff/diff_slice.go
deleted file mode 100644
index 27143fa..0000000
--- a/vendor/github.com/r3labs/diff/diff_slice.go
+++ /dev/null
@@ -1,131 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-func (d *Differ) diffSlice(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if comparative(a, b) {
-		return d.diffSliceComparative(path, a, b)
-	}
-
-	return d.diffSliceGeneric(path, a, b)
-}
-
-func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error {
-	missing := NewComparativeList()
-
-	slice := sliceTracker{}
-	for i := 0; i < a.Len(); i++ {
-		ae := a.Index(i)
-
-		if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae)) {
-			missing.addA(i, &ae)
-		}
-	}
-
-	slice = sliceTracker{}
-	for i := 0; i < b.Len(); i++ {
-		be := b.Index(i)
-
-		if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be)) {
-			missing.addB(i, &be)
-		}
-	}
-
-	// fallback to comparing based on order in slice if item is missing
-	if len(missing.keys) == 0 {
-		return nil
-	}
-
-	return d.diffComparative(path, missing)
-}
-
-func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error {
-	c := NewComparativeList()
-
-	for i := 0; i < a.Len(); i++ {
-		ae := a.Index(i)
-		ak := getFinalValue(ae)
-
-		id := identifier(ak)
-		if id != nil {
-			c.addA(id, &ae)
-		}
-	}
-
-	for i := 0; i < b.Len(); i++ {
-		be := b.Index(i)
-		bk := getFinalValue(be)
-
-		id := identifier(bk)
-		if id != nil {
-			c.addB(id, &be)
-		}
-	}
-
-	return d.diffComparative(path, c)
-}
-
-// keeps track of elements that have already been matched, to stop duplicate matches from occuring
-type sliceTracker []bool
-
-func (st *sliceTracker) has(s, v reflect.Value) bool {
-	if len(*st) != s.Len() {
-		(*st) = make([]bool, s.Len(), s.Len())
-	}
-
-	for i := 0; i < s.Len(); i++ {
-		// skip already matched elements
-		if (*st)[i] {
-			continue
-		}
-
-		x := s.Index(i)
-		if reflect.DeepEqual(x.Interface(), v.Interface()) {
-			(*st)[i] = true
-			return true
-		}
-	}
-
-	return false
-}
-
-func getFinalValue(t reflect.Value) reflect.Value {
-	switch t.Kind() {
-	case reflect.Interface:
-		return getFinalValue(t.Elem())
-	case reflect.Ptr:
-		return getFinalValue(reflect.Indirect(t))
-	default:
-		return t
-	}
-}
-
-func hasAtSameIndex(s, v reflect.Value, atIndex int) bool {
-	// check the element in the slice at atIndex to see if it matches value, if it is a valid index into the slice
-	if atIndex < s.Len() {
-		x := s.Index(atIndex)
-		return reflect.DeepEqual(x.Interface(), v.Interface())
-	}
-
-	return false
-}
diff --git a/vendor/github.com/r3labs/diff/diff_string.go b/vendor/github.com/r3labs/diff/diff_string.go
deleted file mode 100644
index fd5b281..0000000
--- a/vendor/github.com/r3labs/diff/diff_string.go
+++ /dev/null
@@ -1,29 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import "reflect"
-
-func (d *Differ) diffString(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if a.String() != b.String() {
-		d.cl.Add(UPDATE, path, a.String(), b.String())
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_struct.go b/vendor/github.com/r3labs/diff/diff_struct.go
deleted file mode 100644
index 2f7baad..0000000
--- a/vendor/github.com/r3labs/diff/diff_struct.go
+++ /dev/null
@@ -1,105 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-	"time"
-)
-
-func (d *Differ) diffStruct(path []string, a, b reflect.Value) error {
-	if AreType(a, b, reflect.TypeOf(time.Time{})) {
-		return d.diffTime(path, a, b)
-	}
-
-	if a.Kind() == reflect.Invalid {
-		if d.DisableStructValues {
-			d.cl.Add(CREATE, path, nil, b.Interface())
-			return nil
-		}
-		return d.structValues(CREATE, path, b)
-	}
-
-	if b.Kind() == reflect.Invalid {
-		if d.DisableStructValues {
-			d.cl.Add(DELETE, path, a.Interface(), nil)
-			return nil
-		}
-		return d.structValues(DELETE, path, a)
-	}
-
-	for i := 0; i < a.NumField(); i++ {
-		field := a.Type().Field(i)
-		tname := tagName(field)
-
-		if tname == "-" || hasTagOption(field, "immutable") {
-			continue
-		}
-
-		if tname == "" {
-			tname = field.Name
-		}
-
-		af := a.Field(i)
-		bf := b.FieldByName(field.Name)
-
-		fpath := copyAppend(path, tname)
-
-		err := d.diff(fpath, af, bf)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (d *Differ) structValues(t string, path []string, a reflect.Value) error {
-	var nd Differ
-
-	if t != CREATE && t != DELETE {
-		return ErrInvalidChangeType
-	}
-
-	if a.Kind() == reflect.Ptr {
-		a = reflect.Indirect(a)
-	}
-
-	if a.Kind() != reflect.Struct {
-		return ErrTypeMismatch
-	}
-
-	x := reflect.New(a.Type()).Elem()
-
-	for i := 0; i < a.NumField(); i++ {
-
-		field := a.Type().Field(i)
-		tname := tagName(field)
-
-		if tname == "-" {
-			continue
-		}
-
-		if tname == "" {
-			tname = field.Name
-		}
-
-		af := a.Field(i)
-		xf := x.FieldByName(field.Name)
-
-		fpath := copyAppend(path, tname)
-
-		err := nd.diff(fpath, xf, af)
-		if err != nil {
-			return err
-		}
-	}
-
-	for i := 0; i < len(nd.cl); i++ {
-		(d.cl) = append(d.cl, swapChange(t, nd.cl[i]))
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_time.go b/vendor/github.com/r3labs/diff/diff_time.go
deleted file mode 100644
index 4f583ff..0000000
--- a/vendor/github.com/r3labs/diff/diff_time.go
+++ /dev/null
@@ -1,36 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-	"time"
-)
-
-func (d *Differ) diffTime(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	// Marshal and unmarshal time type will lose accuracy. Using unix nano to compare time type.
-	au := a.Interface().(time.Time).UnixNano()
-	bu := b.Interface().(time.Time).UnixNano()
-
-	if au != bu {
-		d.cl.Add(UPDATE, path, a.Interface(), b.Interface())
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/diff_uint.go b/vendor/github.com/r3labs/diff/diff_uint.go
deleted file mode 100644
index 1e16cb7..0000000
--- a/vendor/github.com/r3labs/diff/diff_uint.go
+++ /dev/null
@@ -1,35 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import (
-	"reflect"
-)
-
-func (d *Differ) diffUint(path []string, a, b reflect.Value) error {
-	if a.Kind() == reflect.Invalid {
-		d.cl.Add(CREATE, path, nil, b.Interface())
-		return nil
-	}
-
-	if b.Kind() == reflect.Invalid {
-		d.cl.Add(DELETE, path, a.Interface(), nil)
-		return nil
-	}
-
-	if a.Kind() != b.Kind() {
-		return ErrTypeMismatch
-	}
-
-	if a.Uint() != b.Uint() {
-		if a.CanInterface() {
-			d.cl.Add(UPDATE, path, a.Interface(), b.Interface())
-		} else {
-			d.cl.Add(UPDATE, path, a.Uint(), b.Uint())
-		}
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/r3labs/diff/filter.go b/vendor/github.com/r3labs/diff/filter.go
deleted file mode 100644
index 12e549a..0000000
--- a/vendor/github.com/r3labs/diff/filter.go
+++ /dev/null
@@ -1,22 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package diff
-
-import "regexp"
-
-func pathmatch(filter, path []string) bool {
-	for i, f := range filter {
-		if len(path) < i+1 {
-			return false
-		}
-
-		matched, _ := regexp.MatchString(f, path[i])
-		if !matched {
-			return false
-		}
-	}
-
-	return true
-}
diff --git a/vendor/github.com/r3labs/diff/go.mod b/vendor/github.com/r3labs/diff/go.mod
deleted file mode 100644
index 7c30699..0000000
--- a/vendor/github.com/r3labs/diff/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module github.com/r3labs/diff
-
-go 1.13
-
-require github.com/stretchr/testify v1.5.1
diff --git a/vendor/github.com/r3labs/diff/go.sum b/vendor/github.com/r3labs/diff/go.sum
deleted file mode 100644
index 331fa69..0000000
--- a/vendor/github.com/r3labs/diff/go.sum
+++ /dev/null
@@ -1,11 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/r3labs/diff/options.go b/vendor/github.com/r3labs/diff/options.go
deleted file mode 100644
index 1807378..0000000
--- a/vendor/github.com/r3labs/diff/options.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package diff
-
-// SliceOrdering determines whether the ordering of items in a slice results in a change
-func SliceOrdering(enabled bool) func(d *Differ) error {
-	return func(d *Differ) error {
-		d.SliceOrdering = enabled
-		return nil
-	}
-}
-
-// DisableStructValues disables populating a separate change for each item in a struct,
-// where the struct is being compared to a nil value
-func DisableStructValues() func(d *Differ) error {
-	return func(d *Differ) error {
-		d.DisableStructValues = true
-		return nil
-	}
-}
-
-// CustomValueDiffers allows you to register custom differs for specific types
-func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error {
-	return func(d *Differ) error {
-		d.customValueDiffers = append(d.customValueDiffers, vd...)
-		return nil
-	}
-}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 946f12e..6476aba 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -103,9 +103,6 @@ github.com/onsi/gomega/types
 # github.com/pkg/errors v0.9.1
 ## explicit
 github.com/pkg/errors
-# github.com/r3labs/diff v1.1.0
-## explicit
-github.com/r3labs/diff
 # github.com/russross/blackfriday/v2 v2.0.1
 github.com/russross/blackfriday/v2
 # github.com/shurcooL/sanitized_anchor_name v1.0.0
@@ -123,6 +120,8 @@ github.com/spf13/cobra
 github.com/spf13/cobra/doc
 # github.com/spf13/pflag v1.0.5
 github.com/spf13/pflag
+# github.com/stretchr/testify v1.5.1
+## explicit
 # github.com/x-cray/logrus-prefixed-formatter v0.5.2
 ## explicit
 github.com/x-cray/logrus-prefixed-formatter


Test patch:
diff --git a/pkg/kev/kev_reconcile_test.go b/pkg/kev/kev_reconcile_test.go
index 25dd3a2..d687f40 100644
--- a/pkg/kev/kev_reconcile_test.go
+++ b/pkg/kev/kev_reconcile_test.go
@@ -31,6 +31,65 @@ var _ = Describe("Reconcile", func() {
 		}
 	})
 
+	Describe("Reconcile changes from overlays", func() {
+		When("the overlay version has been updated", func() {
+			BeforeEach(func() {
+				workingDir = "testdata/reconcile-overlay-rollback"
+				source, _ = kev.NewComposeProject([]string{workingDir + "/docker-compose.yaml"})
+				overlayFiles = []string{workingDir + "/docker-compose.kev.dev.yaml"}
+				reporter = bytes.Buffer{}
+			})
+
+			It("confirms the version pre reconciliation", func() {
+				Expect(overlay.GetVersion()).NotTo(Equal(source.GetVersion()))
+			})
+
+			It("should roll back the change", func() {
+				Expect(env.GetVersion()).To(Equal(source.GetVersion()))
+			})
+		})
+
+		Context("for services and volumes", func() {
+			BeforeEach(func() {
+				workingDir = "testdata/reconcile-overlay-keep"
+				overlayFiles = []string{workingDir + "/docker-compose.kev.dev.yaml"}
+				reporter = bytes.Buffer{}
+			})
+
+			When("the overlay service label overrides have been updated", func() {
+				It("confirms the values pre reconciliation", func() {
+					s, _ := overlay.GetService("db")
+					Expect(s.Labels["kev.workload.cpu"]).To(Equal("0.5"))
+					Expect(s.Labels["kev.workload.max-cpu"]).To(Equal("0.75"))
+					Expect(s.Labels["kev.workload.memory"]).To(Equal("50Mi"))
+					Expect(s.Labels["kev.workload.replicas"]).To(Equal("5"))
+					Expect(s.Labels["kev.workload.service-account-name"]).To(Equal("overridden-service-account-name"))
+				})
+
+				It("keeps overridden overlay values", func() {
+					s, _ := env.GetService("db")
+					Expect(s.Labels["kev.workload.cpu"]).To(Equal("0.5"))
+					Expect(s.Labels["kev.workload.max-cpu"]).To(Equal("0.75"))
+					Expect(s.Labels["kev.workload.memory"]).To(Equal("50Mi"))
+					Expect(s.Labels["kev.workload.replicas"]).To(Equal("5"))
+					Expect(s.Labels["kev.workload.service-account-name"]).To(Equal("overridden-service-account-name"))
+				})
+			})
+
+			When("the overlay volume label overrides have been updated", func() {
+				It("confirms the values pre reconciliation", func() {
+					v, _ := overlay.Volumes["db_data"]
+					Expect(v.Labels["kev.volume.size"]).To(Equal("200Mi"))
+				})
+
+				It("keeps overridden overlay values", func() {
+					v, _ := env.GetVolume("db_data")
+					Expect(v.Labels["kev.volume.size"]).To(Equal("200Mi"))
+				})
+			})
+		})
+	})
+
 	Describe("Reconciling changes from sources", func() {
 
 		Context("when the compose version has been updated", func() {
diff --git a/pkg/kev/testdata/reconcile-overlay-keep/docker-compose.kev.dev.yaml b/pkg/kev/testdata/reconcile-overlay-keep/docker-compose.kev.dev.yaml
new file mode 100644
index 0000000..ee69fa3
--- /dev/null
+++ b/pkg/kev/testdata/reconcile-overlay-keep/docker-compose.kev.dev.yaml
@@ -0,0 +1,25 @@
+version: "3.7"
+services:
+  db:
+    labels:
+      kev.service.type: ClusterIP
+      kev.workload.cpu: "0.5"
+      kev.workload.image-pull-policy: IfNotPresent
+      kev.workload.liveness-probe-command: '["CMD", "echo", "Define healthcheck command for service db"]'
+      kev.workload.liveness-probe-disabled: "false"
+      kev.workload.liveness-probe-initial-delay: 1m0s
+      kev.workload.liveness-probe-interval: 1m0s
+      kev.workload.liveness-probe-retries: "3"
+      kev.workload.liveness-probe-timeout: 10s
+      kev.workload.max-cpu: "0.75"
+      kev.workload.max-memory: 500Mi
+      kev.workload.memory: 50Mi
+      kev.workload.replicas: "5"
+      kev.workload.rolling-update-max-surge: "1"
+      kev.workload.service-account-name: overridden-service-account-name
+      kev.workload.type: StatefulSet
+volumes:
+  db_data:
+    labels:
+      kev.volume.size: 200Mi
+      kev.volume.storage-class: standard
diff --git a/pkg/kev/testdata/reconcile-overlay-keep/docker-compose.yaml b/pkg/kev/testdata/reconcile-overlay-keep/docker-compose.yaml
new file mode 100644
index 0000000..2146aa0
--- /dev/null
+++ b/pkg/kev/testdata/reconcile-overlay-keep/docker-compose.yaml
@@ -0,0 +1,19 @@
+version: '3.7'
+services:
+  db:
+    image: mysql:8.0.19
+    command: '--default-authentication-plugin=mysql_native_password'
+    restart: always
+    volumes:
+      - db_data:/var/lib/mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=somewordpress
+      - MYSQL_DATABASE=wordpress
+      - MYSQL_USER=wordpress
+      - MYSQL_PASSWORD=wordpress
+    ports:
+      - "3306"
+volumes:
+  db_data:
+
+
diff --git a/pkg/kev/testdata/reconcile-overlay-keep/kev.yaml b/pkg/kev/testdata/reconcile-overlay-keep/kev.yaml
new file mode 100644
index 0000000..8fb7b9d
--- /dev/null
+++ b/pkg/kev/testdata/reconcile-overlay-keep/kev.yaml
@@ -0,0 +1,4 @@
+compose:
+  - testdata/reconcile-overlay-keep/docker-compose.yaml
+environments:
+  dev: testdata/reconcile-overlay-keep/docker-compose.kev.dev.yaml
diff --git a/pkg/kev/testdata/reconcile-overlay-rollback/docker-compose.kev.dev.yaml b/pkg/kev/testdata/reconcile-overlay-rollback/docker-compose.kev.dev.yaml
new file mode 100644
index 0000000..46281fe
--- /dev/null
+++ b/pkg/kev/testdata/reconcile-overlay-rollback/docker-compose.kev.dev.yaml
@@ -0,0 +1,25 @@
+version: "3.9"
+services:
+  db:
+    labels:
+      kev.service.type: None
+      kev.workload.cpu: "0.1"
+      kev.workload.image-pull-policy: IfNotPresent
+      kev.workload.liveness-probe-command: '["CMD", "echo", "Define healthcheck command for service db"]'
+      kev.workload.liveness-probe-disabled: "false"
+      kev.workload.liveness-probe-initial-delay: 1m0s
+      kev.workload.liveness-probe-interval: 1m0s
+      kev.workload.liveness-probe-retries: "3"
+      kev.workload.liveness-probe-timeout: 10s
+      kev.workload.max-cpu: "0.5"
+      kev.workload.max-memory: 500Mi
+      kev.workload.memory: 10Mi
+      kev.workload.replicas: "1"
+      kev.workload.rolling-update-max-surge: "1"
+      kev.workload.service-account-name: default
+      kev.workload.type: StatefulSet
+volumes:
+  db_data:
+    labels:
+      kev.volume.size: 100Mi
+      kev.volume.storage-class: standard
diff --git a/pkg/kev/testdata/reconcile-overlay-rollback/docker-compose.yaml b/pkg/kev/testdata/reconcile-overlay-rollback/docker-compose.yaml
new file mode 100644
index 0000000..f47584f
--- /dev/null
+++ b/pkg/kev/testdata/reconcile-overlay-rollback/docker-compose.yaml
@@ -0,0 +1,17 @@
+version: '3.7'
+services:
+  db:
+    image: mysql:8.0.19
+    command: '--default-authentication-plugin=mysql_native_password'
+    restart: always
+    volumes:
+      - db_data:/var/lib/mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=somewordpress
+      - MYSQL_DATABASE=wordpress
+      - MYSQL_USER=wordpress
+      - MYSQL_PASSWORD=wordpress
+volumes:
+  db_data:
+
+
diff --git a/pkg/kev/testdata/reconcile-overlay-rollback/kev.yaml b/pkg/kev/testdata/reconcile-overlay-rollback/kev.yaml
new file mode 100644
index 0000000..5c957ea
--- /dev/null
+++ b/pkg/kev/testdata/reconcile-overlay-rollback/kev.yaml
@@ -0,0 +1,4 @@
+compose:
+  - testdata/reconcile-overlay-rollback/docker-compose.yaml
+environments:
+  dev: testdata/reconcile-overlay-rollback/docker-compose.kev.dev.yaml