Repository: roboll/helmfile
Pull request #928 (2019-11-01 17:27:52)

Set HelmBinary in execer constructor
This overrides the default helm command, if provided, as soon as possible.
This way it is already used in `visitStates`.


I ran into an issue using `HELM3` and `--helm-binary` together with helm-secrets. 
I previously used tillerless, which i could now remove. This however caused `DecryptSecret` to fail, as it would still use the helm2 binary; because it runs before the first `helm.SetHelmBinary` call.

While helm-secrets is not fully helm3 compatible yet, its uses within helmfile are, and i was able to just install it as a helm3 plugin. 
   


Patch:
diff --git a/go.mod b/go.mod
index 516e487..340efe7 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/Masterminds/goutils v1.1.0 // indirect
 	github.com/Masterminds/semver v1.4.2
 	github.com/Masterminds/sprig v2.22.0+incompatible
+	github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
 	github.com/aws/aws-sdk-go v1.25.22 // indirect
 	github.com/go-test/deep v1.0.3
 	github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
diff --git a/go.sum b/go.sum
index f3701d3..ad20481 100644
--- a/go.sum
+++ b/go.sum
@@ -15,11 +15,15 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj
 cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
 cloud.google.com/go v0.47.0 h1:1JUtpcY9E7+eTospEwWS2QXP3DEn7poB3E2j0jN74mM=
 cloud.google.com/go v0.47.0/go.mod h1:5p3Ky/7f3N10VBkhuR5LFtddroTiMyjZV/Kj5qOQFxU=
+cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4=
 cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
 code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
 contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
 dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
@@ -37,6 +41,7 @@ github.com/Azure/azure-sdk-for-go v34.4.0+incompatible h1:NQG6PyG1+y9lL5SLWy+urw
 github.com/Azure/azure-sdk-for-go v34.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v35.0.0+incompatible h1:PkmdmQUmeSdQQ5258f4SyCf2Zcz0w67qztEg37cOR7U=
 github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
 github.com/Azure/go-autorest v13.2.0+incompatible h1:jK9tnWNPzdvf5gTuueYWLCfIvAhg15iZ2f6+qkD43s8=
@@ -68,6 +73,7 @@ github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSW
 github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
 github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
 github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc=
 github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
 github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
 github.com/Azure/go-autorest/autorest/to v0.3.0 h1:zebkZaadz7+wIQYgC7GXaz3Wb28yKYfVkkBKwc38VF8=
@@ -84,6 +90,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
 github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
 github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@@ -93,9 +100,13 @@ github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITg
 github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Microsoft/go-winio v0.4.13 h1:Hmi80lzZuI/CaYmlJp/b+FjZdRZhKu9c2mDVqKlLWVs=
 github.com/Microsoft/go-winio v0.4.13/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
+github.com/SAP/go-hdb v0.14.1 h1:hkw4ozGZ/i4eak7ZuGkY5e0hxiXFdNUBNhr4AvZVNFE=
 github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPEkMo=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -103,6 +114,7 @@ github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJM
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190412020505-60e2075261b6/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA=
+github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f h1:oRD16bhpKNAanfcDDVU+J0NXqsgHIvGbbe/sy+r6Rs0=
 github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
 github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
@@ -110,10 +122,15 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
 github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2/go.mod h1:OMVSB21p9+xQUIqlGizHPZfjK+SHws1ht+ZytVDoz9U=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
 github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
 github.com/armon/go-proxyproto v0.0.0-20190211145416-68259f75880e/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
 github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY=
 github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
@@ -126,30 +143,42 @@ github.com/aws/aws-sdk-go v1.25.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi
 github.com/aws/aws-sdk-go v1.25.22 h1:DXcA0jjMnGt2awoWM2qwCu+ouGDB5FYnGxCVrRweE/0=
 github.com/aws/aws-sdk-go v1.25.22/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc=
 github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/briankassouf/jose v0.9.2-0.20180619214549-d2569464773f h1:ZMEzE7R0WNqgbHplzSBaYJhJi5AZWTCK9baU0ebzG6g=
 github.com/briankassouf/jose v0.9.2-0.20180619214549-d2569464773f/go.mod h1:HQhVmdUf7dBNwIIdBTivnCDxcf6IZY3/zrb+uKSJz6Y=
+github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/centrify/cloud-golang-sdk v0.0.0-20190214225812-119110094d0f h1:gJzxrodnNd/CtPXjO3WYiakyNzHg3rtAi7rO74ejHYU=
 github.com/centrify/cloud-golang-sdk v0.0.0-20190214225812-119110094d0f/go.mod h1:C0rtzmGXgN78pYR0tGJFhtHgkbAs0lIbHwkB81VxDQE=
 github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
+github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0 h1:CWU8piLyqoi9qXEUwzOh5KFKGgmSU5ZhktJyYcq6ryQ=
 github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0/go.mod h1:5d8DqS60xkj9k3aXfL3+mXBH0DPYO0FQjcKosxl+b/Q=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
+github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
 github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
 github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -158,43 +187,58 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20190412130859-3b1d194e553a h1:yJ2kD1BvM28M4gt31MuDr0ROKsW+v6zBk9G0Bcr8qAY=
 github.com/denisenkom/go-mssqldb v0.0.0-20190412130859-3b1d194e553a/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
 github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
 github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 h1:2MIhn2R6oXQbgW5yHfS+d6YqyMfXiu2L55rFZC4UD/M=
 github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
 github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/frankban/quicktest v1.4.1 h1:Wv2VwvNn73pAdFIVUQRXYDFp31lXKbqblIXo/Q5GPSg=
 github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/gammazero/deque v0.0.0-20190130191400-2afb3858e9c7 h1:D2LrfOPgGHQprIxmsTpxtzhpmF66HoM6rXSmcqaX7h8=
 github.com/gammazero/deque v0.0.0-20190130191400-2afb3858e9c7/go.mod h1:GeIq9qoE43YdGnDXURnmKTnGg15pQz4mYkXSTChbneI=
+github.com/gammazero/workerpool v0.0.0-20190406235159-88d534f22b56 h1:VzbudKn/nvxYKOdzgkEBS6SSreRjAgoJ+ZeS4wPFkgc=
 github.com/gammazero/workerpool v0.0.0-20190406235159-88d534f22b56/go.mod h1:w9RqFVO2BM3xwWEcAB8Fwp0OviTBBEiRmSBDfbXnd3w=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
 github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
 github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
@@ -202,10 +246,12 @@ github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr6
 github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
 github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/gocql/gocql v0.0.0-20190402132108-0e1d5de854df h1:fwXmhM0OqixzJDOGgTSyNH9eEDij9uGTXwsyWXvyR0A=
 github.com/gocql/gocql v0.0.0-20190402132108-0e1d5de854df/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
 github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@@ -233,14 +279,18 @@ github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
 github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
 github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -261,13 +311,16 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY=
 github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gosuri/uitable v0.0.3 h1:9ZY4qCODg6JL1Ui4dL9LqCF4ghWnAOSV2h7xG98SkHE=
 github.com/gosuri/uitable v0.0.3/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
+github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
 github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
 github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=
 github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
@@ -278,9 +331,12 @@ github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpg
 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
+github.com/hashicorp/consul/api v1.0.1 h1:LkHu3cLXjya4lgrAyZVe/CUBXgJ7AcDWKSeCjAYN9w0=
 github.com/hashicorp/consul/api v1.0.1/go.mod h1:LQlewHPiuaRhn1mP2XE4RrjnlRgOeWa/ZM0xWLCen2M=
 github.com/hashicorp/consul/sdk v0.1.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY=
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -288,6 +344,7 @@ github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6K
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-gcp-common v0.5.0 h1:kkIQTjNTopn4eXQ1+lCiHYZXUtgIZvbc6YtAQkMnTos=
 github.com/hashicorp/go-gcp-common v0.5.0/go.mod h1:IDGUI2N/OS3PiU4qZcXJeWKPI6O/9Y8hOrbSiMcqyYw=
 github.com/hashicorp/go-getter v1.3.0 h1:pFMSFlI9l5NaeuzkpE3L7BYk9qQ9juTAgXW/H0cqxcU=
 github.com/hashicorp/go-getter v1.3.0/go.mod h1:/O1k/AizTN0QmfEKknCYGvICeyKUDqCYA8vvWtGWDeQ=
@@ -297,14 +354,19 @@ github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj
 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc=
 github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-memdb v1.0.2 h1:AIjzJlwIxz2inhZqRJZfe6D15lPeF0/cZyS1BVlnlHg=
 github.com/hashicorp/go-memdb v1.0.2/go.mod h1:I6dKdmYhZqU0RJSheVEWgTNWdVQH5QvTgIUQ0t/t32M=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
 github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
+github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
 github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
+github.com/hashicorp/go-raftchunking v0.6.2 h1:imj6CVkwXj6VzgXZQvzS+fSrkbFCzlJ2t00F3PacnuU=
 github.com/hashicorp/go-raftchunking v0.6.2/go.mod h1:cGlg3JtDy7qy6c/3Bu660Mic1JF+7lWqIwCFSb08fX0=
 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
 github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE=
@@ -323,6 +385,7 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S
 github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
 github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
@@ -338,30 +401,50 @@ 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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/nomad/api v0.0.0-20190412184103-1c38ced33adf h1:U/40PQvWkaXCDdK9QHKf1pVDVcA+NIDVbzzonFGkgIA=
 github.com/hashicorp/nomad/api v0.0.0-20190412184103-1c38ced33adf/go.mod h1:BDngVi1f4UA6aJq9WYTgxhfWSE1+42xshvstLU2fRGk=
 github.com/hashicorp/raft v1.0.1/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI=
+github.com/hashicorp/raft v1.1.1 h1:HJr7UE1x/JrJSc9Oy6aDBHtNHUUBHjcQjTgvUVihoZs=
 github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=
 github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=
+github.com/hashicorp/raft-snapshot v1.0.1 h1:cx002JsTEAfAP0pIuANlDtTXg/pi2Db6YbRRmLQTQKw=
 github.com/hashicorp/raft-snapshot v1.0.1/go.mod h1:5sL9eUn72lH5DzsFIJ9jaysITbHksSSszImWSOTC8Ic=
+github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/hashicorp/vault v1.2.3 h1:kTMygKuUEjFSp5Xzfkw7R2ScJcOG4rsAcDnPtdrOBsE=
 github.com/hashicorp/vault v1.2.3/go.mod h1:3foqP78mwtT8iZoKtBt41XnUpL7/etJxg+GQQX+6p64=
+github.com/hashicorp/vault-plugin-auth-alicloud v0.5.2-0.20190814210027-93970f08f2ec h1:HXVE8h6RXFsPJgwWpE+5CscsgekqtX4nhDlZGV9jEe4=
 github.com/hashicorp/vault-plugin-auth-alicloud v0.5.2-0.20190814210027-93970f08f2ec/go.mod h1:TYFfVFgKF9x92T7uXouI9rLPkNnyXo/KkNcj5t+mjdM=
+github.com/hashicorp/vault-plugin-auth-azure v0.5.2-0.20190814210035-08e00d801115 h1:E57y918o+c+NoI5k7ohbpZu7vRm1XZKZfC5VQVpJvDI=
 github.com/hashicorp/vault-plugin-auth-azure v0.5.2-0.20190814210035-08e00d801115/go.mod h1:sRhTnkcbjJgPeES0ddCTq8S2waSakyMiWLUwO5J/Wjk=
+github.com/hashicorp/vault-plugin-auth-centrify v0.5.2-0.20190814210042-090ec2ed93ce h1:X8umWdCqSVk/75ZjEBDxYL+V8i+jK3KbJbFoyOryCww=
 github.com/hashicorp/vault-plugin-auth-centrify v0.5.2-0.20190814210042-090ec2ed93ce/go.mod h1:WstOCHERNbk2dblnY5MV9Qeh/hzTSQpVs5xPuyAzlBo=
+github.com/hashicorp/vault-plugin-auth-cf v0.0.0-20190821162840-1c2205826fee h1:gJG1PJGiqi+0M0HTYlwDyV5CyetLhFl9DxyMJre5H9Y=
 github.com/hashicorp/vault-plugin-auth-cf v0.0.0-20190821162840-1c2205826fee/go.mod h1:zOag32+pm1R4FFNhXMLP506Oesjoai3gHEEpxqUaTr0=
 github.com/hashicorp/vault-plugin-auth-gcp v0.5.1/go.mod h1:eLj92eX8MPI4vY1jaazVLF2sVbSAJ3LRHLRhF/pUmlI=
+github.com/hashicorp/vault-plugin-auth-gcp v0.5.2-0.20190814210049-1ccb3dc10102 h1:RTHVdxCDwxTq/4zZFkV+b8zexkSU5EOXkY2D/kAvyFU=
 github.com/hashicorp/vault-plugin-auth-gcp v0.5.2-0.20190814210049-1ccb3dc10102/go.mod h1:j0hMnnTD44zXGQhLM1jarYDaTmSp6OPiOzgFQ6mNgzc=
+github.com/hashicorp/vault-plugin-auth-jwt v0.5.2-0.20190815164639-5fa0eef3a023 h1:RMGN5WLZ6QnTGNsDT5jmTf3RO54lOV7JMeveLMngOuk=
 github.com/hashicorp/vault-plugin-auth-jwt v0.5.2-0.20190815164639-5fa0eef3a023/go.mod h1:Ti2NPndKhSGpSL6gWg11n7TkmuI7318BIPeojayIVRU=
+github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.2-0.20190826163451-8461c66275a9 h1:PjbIf3mlPBJopQSJstQAhVbdGTVZ/W35RZtm/GCOTUs=
 github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.2-0.20190826163451-8461c66275a9/go.mod h1:qkrONCr71ckSCTItJQ1j9uet/faieZJ5c7+GZugTm7s=
+github.com/hashicorp/vault-plugin-auth-oci v0.0.0-20190904175623-97c0c0187c5c h1:z6LQZvs1OtoVy2XgbgNhiDgp0U62Xbstn7/cgNZvh6g=
 github.com/hashicorp/vault-plugin-auth-oci v0.0.0-20190904175623-97c0c0187c5c/go.mod h1:YAl51RsYRihPbSdnug1NsvutzbRVfrZ12FjEIvSiOTs=
+github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190814210117-e079e01fbb93 h1:kXTV1ImOPgDGZxAlbEQfiXgnZY/34vfgnZVhI/tscmg=
 github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190814210117-e079e01fbb93/go.mod h1:N9XpfMXjeLHBgUd8iy4avOC4mCSqUC7B/R8AtCYhcfE=
+github.com/hashicorp/vault-plugin-secrets-ad v0.5.3-0.20190814210122-0f2fd536b250 h1:+mm2cM5msg/USImbvnMS2yzCMBYMCO3CrvsATWGtHtY=
 github.com/hashicorp/vault-plugin-secrets-ad v0.5.3-0.20190814210122-0f2fd536b250/go.mod h1:F8hKHqcB7stN2OhnqE3emwFYtKO0IDNxMBbPs2n8vr0=
+github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.2-0.20190814210129-4d18bec92f56 h1:PGE26//x1eiAbZ1ExffhKa4y9xgDKLd9BHDZRkOzbEY=
 github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.2-0.20190814210129-4d18bec92f56/go.mod h1:hJ42zFd3bHyE8O2liBUG+VPY0JxdMrj51TOwVGViUIU=
+github.com/hashicorp/vault-plugin-secrets-azure v0.5.2-0.20190814210135-54b8afbc42ae h1:LtRJy7H/9ftjHGo5SMLG8/7DI7CYL1Zur9jBJTyzXg8=
 github.com/hashicorp/vault-plugin-secrets-azure v0.5.2-0.20190814210135-54b8afbc42ae/go.mod h1:SBc53adxMmf+o8zqRbqYvq+nuSrz8OHYmgmPfxVMJEo=
+github.com/hashicorp/vault-plugin-secrets-gcp v0.5.3-0.20190814210141-d2086ff79b04 h1:2FLjwVqpWueSoxaNdcC2Za7RX8FNp8Xt8pF/03dinV4=
 github.com/hashicorp/vault-plugin-secrets-gcp v0.5.3-0.20190814210141-d2086ff79b04/go.mod h1:Sc+ba3kscakE5a/pi8JJhWvXWok3cpt1P77DApmUuDc=
+github.com/hashicorp/vault-plugin-secrets-gcpkms v0.5.2-0.20190814210149-315cdbf5de6e h1:RjQBOFneGwxhHsymNtbEUJXAjMO74GlZcmUrGqJnYxY=
 github.com/hashicorp/vault-plugin-secrets-gcpkms v0.5.2-0.20190814210149-315cdbf5de6e/go.mod h1:5prAHuCcBiyv+xfGBviTVYeDQUhmQYN7WrxC2gMRWeQ=
+github.com/hashicorp/vault-plugin-secrets-kv v0.5.2-0.20190814210155-e060c2a001a8 h1:8nZOMqGQQiuWNld162nxUvM4/7EW4NOO9gpyp7LCC84=
 github.com/hashicorp/vault-plugin-secrets-kv v0.5.2-0.20190814210155-e060c2a001a8/go.mod h1:5ksi71TrWxz7ZRo0MIlsry2lYDlZQyLalN4cF8a4vnk=
 github.com/hashicorp/vault/api v1.0.1/go.mod h1:AV/+M5VPDpB90arloVX0rVDUIHkONiwz5Uza9HRtpUE=
 github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
@@ -376,22 +459,27 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20190814205504-1cad00d1133b/go.mod h1:B
 github.com/hashicorp/vault/sdk v0.1.14-0.20190909201848-e0fbf9b652e2 h1:b65cSyZqljnCPzzsUXvR4P0eXypo1xahQyG809+IySk=
 github.com/hashicorp/vault/sdk v0.1.14-0.20190909201848-e0fbf9b652e2/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
 github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
 github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=
 github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
 github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
 github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4 h1:3K3KcD4S6/Y2hevi70EzUTNKOS3cryQyhUnkjE6Tz0w=
 github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
 github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
+github.com/jeffchao/backoff v0.0.0-20140404060208-9d7fd7aa17f2 h1:mex1izRBCD+7WjieGgRdy7e651vD/lvB1bD9vNE/3K4=
 github.com/jeffchao/backoff v0.0.0-20140404060208-9d7fd7aa17f2/go.mod h1:xkfESuHriIekR+4RoV+fu91j/CfnYM29Zi2tMFw5iD4=
 github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00=
+github.com/jefferai/jsonx v1.0.0 h1:Xoz0ZbmkpBvED5W9W1B5B/zc3Oiq7oXqiW7iRV3B6EI=
 github.com/jefferai/jsonx v1.0.0/go.mod h1:OGmqmi2tTeI/PS+qQfBDToLHHJIy/RMp24fPo8vFvoQ=
 github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
@@ -402,12 +490,15 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
 github.com/joyent/triton-go v0.0.0-20190112182421-51ffac552869/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA=
 github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f h1:Gsc9mVHLRqBjMgdQCghN9NObCcRncDqxJvBvEaIIQEo=
 github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -416,13 +507,16 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.0.0/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw=
 github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
 github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -438,9 +532,12 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
 github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/michaelklishin/rabbit-hole v1.5.0 h1:Bex27BiFDsijCM9D0ezSHqyy0kehpYHuNKaPqq/a4RM=
 github.com/michaelklishin/rabbit-hole v1.5.0/go.mod h1:vvI1uOitYZi0O5HEGXhaWC1XT80Gy+HvFheJ+5Krlhk=
 github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
+github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
@@ -459,11 +556,14 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8 h1:1CO5wil3HuiVLrUQ2ovSTO+6AfNOA5EMkHHVyHE9IwA=
 github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8=
 github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mozilla-services/yaml v0.0.0-20180922153656-28ffe5d0cafb h1:wj4n5+b4t84Qze8N/n0PKpaBTlbA7g7nTYG01h16mh0=
 github.com/mozilla-services/yaml v0.0.0-20180922153656-28ffe5d0cafb/go.mod h1:Is/Ucts/yU/mWyGR8yELRoO46mejouKsJfQLAIfTR18=
@@ -472,24 +572,35 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
 github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
 github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
 github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
 github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
 github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/oracle/oci-go-sdk v7.0.0+incompatible h1:oj5ESjXwwkFRdhZSnPlShvLWYdt/IZ65RQxveYM3maA=
 github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
+github.com/ory/dockertest v3.3.4+incompatible h1:VrpM6Gqg7CrPm3bL4Wm1skO+zFWLbh7/Xb5kGEbJRh8=
 github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
+github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso=
 github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -504,23 +615,29 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/pquerna/otp v1.1.0 h1:q2gMsMuMl3JzneUaAX1MRGxLvOG6bzXV51hivBaStf0=
 github.com/pquerna/otp v1.1.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk=
 github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU=
 github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20180612222113-7d6f385de8be/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad h1:j5pg/OewZJyE6i3hIG4v3eQUvUyFdQkC8Nd/mjaEkxE=
 github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad/go.mod h1:ozniNEFS3j1qCwHKdvraMn1WJOsUxHd7lYfukEIS4cs=
@@ -533,8 +650,11 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
 github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
@@ -565,21 +685,27 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI=
 github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
 github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 h1:0ngsPmuP6XIjiFRNFYlvKwSr5zff2v+uPHaffZ6/M4k=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 h1:BhIUXV2ySTLrKgh/Hnts+QTQlIbWtomXt3LMdzME0A0=
@@ -603,6 +729,7 @@ github.com/variantdev/vals v0.0.0-20191026125821-5d18b16cf30a/go.mod h1:8CW8eonQ
 github.com/variantdev/vals v0.0.0-20191030045026-1fa6af1b5299 h1:91g7EEeE6dHdubLkS013s9vcZScrGDk/RqZEQwSkn4w=
 github.com/variantdev/vals v0.0.0-20191030045026-1fa6af1b5299/go.mod h1:8CW8eonQlIJgAjF1fLfrkaBe16fGjwGf2PX52/Vualw=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20190412021913-f29b1ada1971/go.mod h1:KSGwdbiFchh5KIC9My2+ZVl5/3ANcwohw50dpPwa2cw=
 go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a h1:N7VD+PwpJME2ZfQT8+ejxwA4Ow10IkGbU0MGf94ll8k=
@@ -715,6 +842,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -856,19 +984,25 @@ google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
 google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
 gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
 gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0=
 gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 gopkg.in/ory-am/dockertest.v3 v3.3.4/go.mod h1:s9mmoLkaGeAh97qygnNj4xWkiN7e1SKekYC6CovU+ek=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
@@ -877,6 +1011,7 @@ gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4
 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
 gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
@@ -901,12 +1036,17 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+k8s.io/api v0.0.0-20190409092523-d687e77c8ae9 h1:c9UEl5z8gk1DGh/g3snETZ+a52YeR9VdbX/3BQ4PHas=
 k8s.io/api v0.0.0-20190409092523-d687e77c8ae9/go.mod h1:FQEUn50aaytlU65qqBn/w+5ugllHwrBzKm7DzbnXdzE=
+k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b h1:fVkKJL9FIpA8LSJyHVM00MP45q1WJ7+af77vcxmQP4g=
 k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b/go.mod h1:FW86P8YXVLsbuplGMZeb20J3jYHscrDqw4jELaFJvRU=
+k8s.io/klog v0.0.0-20190306015804-8e90cee79f82 h1:SHucoAy7lRb+w5oC/hbXyZg+zX+Wftn6hD4tGzHCVqA=
 k8s.io/klog v0.0.0-20190306015804-8e90cee79f82/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
+layeh.com/radius v0.0.0-20190322222518-890bc1058917 h1:BDXFaFzUt5EIqe/4wrTc4AcYZWP6iC6Ult+jQWLh5eU=
 layeh.com/radius v0.0.0-20190322222518-890bc1058917/go.mod h1:fywZKyu//X7iRzaxLgPWsvc0L26IUpVvE/aeIL2JtIQ=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
 sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
diff --git a/main.go b/main.go
index 7dd95ea..25cf6b7 100644
--- a/main.go
+++ b/main.go
@@ -50,6 +50,7 @@ func main() {
 		cli.StringFlag{
 			Name:  "helm-binary, b",
 			Usage: "path to helm binary",
+			Value: "helm",
 		},
 		cli.StringFlag{
 			Name:  "file, f",
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 2745eb4..a0a8ed1 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -1,6 +1,7 @@
 package app
 
 import (
+	"bytes"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -8,8 +9,10 @@ import (
 	"os/signal"
 	"strings"
 	"syscall"
+	"text/tabwriter"
 
 	"github.com/gosuri/uitable"
+	"github.com/roboll/helmfile/pkg/argparser"
 	"github.com/roboll/helmfile/pkg/helmexec"
 	"github.com/roboll/helmfile/pkg/remote"
 	"github.com/roboll/helmfile/pkg/state"
@@ -33,7 +36,6 @@ type App struct {
 	Env         string
 	Namespace   string
 	Selectors   []string
-	HelmBinary  string
 	Args        string
 	ValuesFiles []string
 	Set         map[string]interface{}
@@ -66,12 +68,11 @@ func New(conf ConfigProvider) *App {
 		Env:         conf.Env(),
 		Namespace:   conf.Namespace(),
 		Selectors:   conf.Selectors(),
-		HelmBinary:  conf.HelmBinary(),
 		Args:        conf.Args(),
 		FileOrDir:   conf.FileOrDir(),
 		ValuesFiles: conf.StateValuesFiles(),
 		Set:         conf.StateValuesSet(),
-		helmExecer: helmexec.New(conf.Logger(), conf.KubeContext(), &helmexec.ShellRunner{
+		helmExecer: helmexec.New(conf.HelmBinary(), conf.Logger(), conf.KubeContext(), &helmexec.ShellRunner{
 			Logger: conf.Logger(),
 		}),
 	})
@@ -146,7 +147,7 @@ func (a *App) Sync(c SyncConfigProvider) error {
 
 func (a *App) Apply(c ApplyConfigProvider) error {
 	return a.ForEachState(func(run *Run) []error {
-		return run.Apply(c)
+		return a.apply(run, c)
 	})
 }
 
@@ -159,13 +160,13 @@ func (a *App) Status(c StatusesConfigProvider) error {
 func (a *App) Delete(c DeleteConfigProvider) error {
 	return a.reverse().ForEachState(func(run *Run) []error {
 		return run.Delete(c)
-	})
+	}, true)
 }
 
 func (a *App) Destroy(c DestroyConfigProvider) error {
 	return a.reverse().ForEachState(func(run *Run) []error {
 		return run.Destroy(c)
-	})
+	}, true)
 }
 
 func (a *App) Test(c TestConfigProvider) error {
@@ -380,8 +381,10 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
 		if tmplErr != nil {
 			return appError(fmt.Sprintf("failed executing release templates in \"%s\"", f), tmplErr)
 		}
+
 		processed, errs := converge(templated, helm)
 		noMatchInHelmfiles = noMatchInHelmfiles && !processed
+
 		return context{a, templated}.clean(errs)
 	})
 
@@ -396,13 +399,13 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
 	return nil
 }
 
-func (a *App) ForEachState(do func(*Run) []error) error {
+func (a *App) ForEachState(do func(*Run) []error, dagEnabled ...bool) error {
 	ctx := NewContext()
 	err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) []error {
 		run := NewRun(st, helm, ctx)
 
 		return do(run)
-	})
+	}, dagEnabled...)
 
 	if err != nil && a.ErrorHandler != nil {
 		return a.ErrorHandler(err)
@@ -411,7 +414,74 @@ func (a *App) ForEachState(do func(*Run) []error) error {
 	return err
 }
 
-func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
+func printBatches(batches [][]state.Release) string {
+	buf := &bytes.Buffer{}
+
+	w := new(tabwriter.Writer)
+
+	w.Init(buf, 0, 1, 1, ' ', 0)
+
+	fmt.Fprintln(w, "GROUP\tRELEASES")
+
+	for i, batch := range batches {
+		ids := []string{}
+		for _, r := range batch {
+			ids = append(ids, state.ReleaseToID(&r.ReleaseSpec))
+		}
+		fmt.Fprintf(w, "%d\t%s\n", i+1, strings.Join(ids, ", "))
+	}
+
+	w.Flush()
+
+	return buf.String()
+}
+
+func withDAG(templated *state.HelmState, helm helmexec.Interface, logger *zap.SugaredLogger, reverse bool, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) (bool, []error) {
+	batches, err := state.PlanReleases(templated.Releases, templated.Selectors, reverse)
+	if err != nil {
+		return false, []error{err}
+	}
+
+	numBatches := len(batches)
+
+	logger.Debugf("processing %d groups of releases in this order:\n%s", numBatches, printBatches(batches))
+
+	all := true
+
+	for i, batch := range batches {
+		var targets []state.ReleaseSpec
+
+		for _, marked := range batch {
+			targets = append(targets, marked.ReleaseSpec)
+		}
+
+		var releaseIds []string
+		for _, r := range targets {
+			releaseIds = append(releaseIds, state.ReleaseToID(&r))
+		}
+
+		logger.Debugf("processing releases in group %d/%d: %s", i+1, numBatches, strings.Join(releaseIds, ", "))
+
+		batchSt := *templated
+		batchSt.Releases = targets
+
+		processed, errs := converge(&batchSt, helm)
+
+		if errs != nil && len(errs) > 0 {
+			return false, errs
+		}
+
+		all = all && processed
+	}
+
+	return all, nil
+}
+
+type Opts struct {
+	DAGEnabled bool
+}
+
+func (a *App) visitStatesWithSelectorsAndRemoteSupport(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
 	opts := LoadOpts{
 		Selectors: a.Selectors,
 	}
@@ -450,7 +520,11 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
 
 	a.remote = remote
 
-	return a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
+	return a.visitStates(fileOrDir, opts, converge)
+}
+
+func (a *App) Wrap(converge func(*state.HelmState, helmexec.Interface) []error) func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
+	return func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
 		if len(st.Selectors) > 0 {
 			err := st.FilterReleases()
 			if err != nil {
@@ -458,10 +532,6 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
 			}
 		}
 
-		if a.HelmBinary != "" {
-			helm.SetHelmBinary(a.HelmBinary)
-		}
-
 		type Key struct {
 			TillerNamespace, Name string
 		}
@@ -485,6 +555,20 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
 		processed := len(st.Releases) != 0 && len(errs) == 0
 
 		return processed, errs
+	}
+}
+
+func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, dagEnabled ...bool) error {
+	f := a.Wrap(converge)
+
+	if len(dagEnabled) > 0 && dagEnabled[0] {
+		return a.visitStatesWithSelectorsAndRemoteSupport(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
+			return withDAG(st, helm, a.Logger, a.Reverse, f)
+		})
+	}
+
+	return a.visitStatesWithSelectorsAndRemoteSupport(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
+		return f(st, helm)
 	})
 }
 
@@ -560,6 +644,159 @@ func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
 	return files, nil
 }
 
+func (a *App) apply(r *Run, c ApplyConfigProvider) []error {
+	st := r.state
+	helm := r.helm
+	ctx := r.ctx
+
+	affectedReleases := state.AffectedReleases{}
+	if !c.SkipDeps() {
+		if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
+			return errs
+		}
+		if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
+			return errs
+		}
+	}
+	if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
+		return errs
+	}
+
+	// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
+	detailedExitCode := true
+
+	diffOpts := &state.DiffOpts{
+		NoColor: c.NoColor(),
+		Context: c.Context(),
+		Set:     c.Set(),
+	}
+
+	changedReleases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false, diffOpts)
+
+	deletingReleases, err := st.DetectReleasesToBeDeleted(helm, st.Releases)
+	if err != nil {
+		errs = append(errs, err)
+	}
+
+	fatalErrs := []error{}
+
+	noError := true
+	for _, e := range errs {
+		switch err := e.(type) {
+		case *state.ReleaseError:
+			if err.Code != 2 {
+				noError = false
+				fatalErrs = append(fatalErrs, e)
+			}
+		default:
+			noError = false
+			fatalErrs = append(fatalErrs, e)
+		}
+	}
+
+	releasesToBeDeleted := map[string]state.ReleaseSpec{}
+	for _, r := range deletingReleases {
+		id := state.ReleaseToID(&r)
+		releasesToBeDeleted[id] = r
+	}
+
+	releasesToBeUpdated := map[string]state.ReleaseSpec{}
+	for _, r := range changedReleases {
+		id := state.ReleaseToID(&r)
+
+		// If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade`
+		if _, ok := releasesToBeDeleted[id]; !ok {
+			releasesToBeUpdated[id] = r
+		}
+	}
+
+	// sync only when there are changes
+	if noError {
+		if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
+			// TODO better way to get the logger
+			logger := c.Logger()
+			logger.Infof("")
+			logger.Infof("No affected releases")
+		} else {
+			names := []string{}
+			for _, r := range releasesToBeUpdated {
+				names = append(names, fmt.Sprintf("  %s (%s) UPDATED", r.Name, r.Chart))
+			}
+			for _, r := range releasesToBeDeleted {
+				names = append(names, fmt.Sprintf("  %s (%s) DELETED", r.Name, r.Chart))
+			}
+			// Make the output deterministic for testing purpose
+			sort.Strings(names)
+
+			infoMsg := fmt.Sprintf(`Affected releases are:
+%s
+`, strings.Join(names, "\n"))
+			confMsg := fmt.Sprintf(`%s
+Do you really want to apply?
+  Helmfile will apply all your changes, as shown above.
+
+`, infoMsg)
+			interactive := c.Interactive()
+			if !interactive {
+				a.Logger.Debug(infoMsg)
+			}
+			if !interactive || interactive && r.askForConfirmation(confMsg) {
+				r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
+
+				// We deleted releases by traversing the DAG in reverse order
+				if len(releasesToBeDeleted) > 0 {
+					_, errs := withDAG(st, helm, a.Logger, true, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
+						var rs []state.ReleaseSpec
+
+						for _, r := range subst.Releases {
+							if _, ok := releasesToBeDeleted[state.ReleaseToID(&r)]; ok {
+								rs = append(rs, r)
+							}
+						}
+
+						subst.Releases = rs
+
+						return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency())
+					}))
+
+					if errs != nil && len(errs) > 0 {
+						return errs
+					}
+				}
+
+				// We upgrade releases by traversing the DAG
+				if len(releasesToBeUpdated) > 0 {
+					_, errs := withDAG(st, helm, a.Logger, false, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
+						var rs []state.ReleaseSpec
+
+						for _, r := range subst.Releases {
+							if _, ok := releasesToBeUpdated[state.ReleaseToID(&r)]; ok {
+								rs = append(rs, r)
+							}
+						}
+
+						subst.Releases = rs
+
+						syncOpts := state.SyncOpts{
+							Set: c.Set(),
+						}
+						return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), &syncOpts)
+					}))
+
+					if errs != nil && len(errs) > 0 {
+						return errs
+					}
+				}
+
+				return nil
+			}
+		}
+	}
+
+	affectedReleases.DisplayAffectedReleases(c.Logger())
+	return fatalErrs
+}
+
 func fileExistsAt(path string) bool {
 	fileInfo, err := os.Stat(path)
 	return err == nil && fileInfo.Mode().IsRegular()
diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go
index 9c029f9..31932a9 100644
--- a/pkg/app/desired_state_file_loader.go
+++ b/pkg/app/desired_state_file_loader.go
@@ -76,10 +76,10 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
 	}
 
 	if ld.namespace != "" {
-		if st.Namespace != "" {
+		if st.OverrideNamespace != "" {
 			return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
 		}
-		st.Namespace = ld.namespace
+		st.OverrideNamespace = ld.namespace
 	}
 
 	return st, nil
diff --git a/pkg/app/run.go b/pkg/app/run.go
index 94477cb..3a8bcbb 100644
--- a/pkg/app/run.go
+++ b/pkg/app/run.go
@@ -118,104 +118,6 @@ Do you really want to delete?
 	return errs
 }
 
-func (r *Run) Apply(c ApplyConfigProvider) []error {
-	st := r.state
-	helm := r.helm
-	ctx := r.ctx
-
-	affectedReleases := state.AffectedReleases{}
-	if !c.SkipDeps() {
-		if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
-			return errs
-		}
-		if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
-			return errs
-		}
-	}
-	if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
-		return errs
-	}
-
-	// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
-	detailedExitCode := true
-
-	diffOpts := &state.DiffOpts{
-		NoColor: c.NoColor(),
-		Context: c.Context(),
-		Set:     c.Set(),
-	}
-
-	releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false, diffOpts)
-
-	releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
-	if err != nil {
-		errs = append(errs, err)
-	}
-
-	fatalErrs := []error{}
-
-	noError := true
-	for _, e := range errs {
-		switch err := e.(type) {
-		case *state.ReleaseError:
-			if err.Code != 2 {
-				noError = false
-				fatalErrs = append(fatalErrs, e)
-			}
-		default:
-			noError = false
-			fatalErrs = append(fatalErrs, e)
-		}
-	}
-
-	// sync only when there are changes
-	if noError {
-		if len(releases) == 0 && len(releasesToBeDeleted) == 0 {
-			// TODO better way to get the logger
-			logger := c.Logger()
-			logger.Infof("")
-			logger.Infof("No affected releases")
-		} else {
-			names := []string{}
-			for _, r := range releases {
-				names = append(names, fmt.Sprintf("  %s (%s) UPDATED", r.Name, r.Chart))
-			}
-			for _, r := range releasesToBeDeleted {
-				names = append(names, fmt.Sprintf("  %s (%s) DELETED", r.Name, r.Chart))
-			}
-
-			msg := fmt.Sprintf(`Affected releases are:
-%s
-
-Do you really want to apply?
-  Helmfile will apply all your changes, as shown above.
-
-`, strings.Join(names, "\n"))
-			interactive := c.Interactive()
-			if !interactive || interactive && r.askForConfirmation(msg) {
-				rs := []state.ReleaseSpec{}
-				for _, r := range releases {
-					rs = append(rs, *r)
-				}
-				for _, r := range releasesToBeDeleted {
-					rs = append(rs, *r)
-				}
-
-				r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
-
-				st.Releases = rs
-				syncOpts := &state.SyncOpts{
-					Set: c.Set(),
-				}
-				return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
-			}
-		}
-	}
-
-	affectedReleases.DisplayAffectedReleases(c.Logger())
-	return fatalErrs
-}
-
 func (r *Run) Diff(c DiffConfigProvider) []error {
 	st := r.state
 	helm := r.helm
diff --git a/pkg/app/two_pass_renderer.go b/pkg/app/two_pass_renderer.go
index 17ab39a..d9710ba 100644
--- a/pkg/app/two_pass_renderer.go
+++ b/pkg/app/two_pass_renderer.go
@@ -31,12 +31,15 @@ func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environmen
 	yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content)
 	if err != nil && r.logger != nil {
 		r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
+		r.logger.Debugf("template syntax error: %v", err)
 		if yamlBuf == nil { // we have a template syntax error, let the second parse report
-			r.logger.Debugf("template syntax error: %v", err)
 			return firstPassEnv, nil
 		}
 	}
 	yamlData := yamlBuf.String()
+	if r.logger != nil {
+		r.logger.Debugf("first-pass rendering output of \"%s\":\n%s", filename, prependLineNumbers(yamlData))
+	}
 
 	// Work-around for https://github.com/golang/go/issues/24963
 	sanitized := strings.ReplaceAll(yamlData, "<no value>", "")
diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go
index 89c3202..1d51bca 100644
--- a/pkg/helmexec/exec.go
+++ b/pkg/helmexec/exec.go
@@ -13,10 +13,6 @@ import (
 	"go.uber.org/zap/zapcore"
 )
 
-const (
-	command = "helm"
-)
-
 type decryptedSecret struct {
 	mutex sync.RWMutex
 	bytes []byte
@@ -50,9 +46,9 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
 }
 
 // New for running helm commands
-func New(logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
+func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
 	return &execer{
-		helmBinary:       command,
+		helmBinary:       helmBinary,
 		logger:           logger,
 		kubeContext:      kubeContext,
 		runner:           runner,
diff --git a/pkg/helmexec/exit_error.go b/pkg/helmexec/exit_error.go
index fd01132..6b11638 100644
--- a/pkg/helmexec/exit_error.go
+++ b/pkg/helmexec/exit_error.go
@@ -8,8 +8,8 @@ import (
 
 func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
 	return ExitError{
-		msg:        fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
-		exitStatus: exitStatus,
+		Message: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
+		Code:    exitStatus,
 	}
 }
 
@@ -23,14 +23,14 @@ func indent(text string) string {
 
 // ExitError is created whenever your shell command exits with a non-zero exit status
 type ExitError struct {
-	msg        string
-	exitStatus int
+	Message string
+	Code    int
 }
 
 func (e ExitError) Error() string {
-	return e.msg
+	return e.Message
 }
 
 func (e ExitError) ExitStatus() int {
-	return e.exitStatus
+	return e.Code
 }
diff --git a/pkg/state/state.go b/pkg/state/state.go
index 29b9194..075f9c2 100644
--- a/pkg/state/state.go
+++ b/pkg/state/state.go
@@ -10,6 +10,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -20,9 +21,6 @@ import (
 	"github.com/roboll/helmfile/pkg/helmexec"
 	"github.com/roboll/helmfile/pkg/remote"
 	"github.com/roboll/helmfile/pkg/tmpl"
-	"github.com/variantdev/dag/pkg/dag"
-
-	"regexp"
 
 	"github.com/tatsushid/go-prettytable"
 	"github.com/variantdev/vals"
@@ -45,7 +43,7 @@ type HelmState struct {
 	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles,omitempty"`
 	DeprecatedContext  string            `yaml:"context,omitempty"`
 	DeprecatedReleases []ReleaseSpec     `yaml:"charts,omitempty"`
-	Namespace          string            `yaml:"namespace,omitempty"`
+	OverrideNamespace  string            `yaml:"namespace,omitempty"`
 	Repositories       []RepositorySpec  `yaml:"repositories,omitempty"`
 	Releases           []ReleaseSpec     `yaml:"releases,omitempty"`
 	Selectors          []string          `yaml:"-"`
@@ -195,6 +193,12 @@ type ReleaseSpec struct {
 	installedVersion string
 }
 
+type Release struct {
+	ReleaseSpec
+
+	Filtered bool
+}
+
 // SetValue are the key values to set on a helm release
 type SetValue struct {
 	Name   string   `yaml:"name,omitempty"`
@@ -217,9 +221,16 @@ const MissingFileHandlerInfo = "Info"
 const MissingFileHandlerWarn = "Warn"
 const MissingFileHandlerDebug = "Debug"
 
-func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
-	if st.Namespace != "" {
-		spec.Namespace = st.Namespace
+func (st *HelmState) ApplyOverrides(spec *ReleaseSpec) {
+	if st.OverrideNamespace != "" {
+		spec.Namespace = st.OverrideNamespace
+
+		for i := 0; i < len(spec.Needs); i++ {
+			n := spec.Needs[i]
+			if len(strings.Split(n, "/")) == 1 {
+				spec.Needs[i] = st.OverrideNamespace + "/" + n
+			}
+		}
 	}
 }
 
@@ -290,7 +301,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
 		},
 		func(workerIndex int) {
 			for release := range jobs {
-				st.applyDefaultsTo(release)
+				st.ApplyOverrides(release)
 
 				// If `installed: false`, the only potential operation on this release would be uninstalling.
 				// We skip generating values files in that case, because for an uninstall with `helm delete`, we don't need to those.
@@ -370,10 +381,10 @@ func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helme
 	return false, nil
 }
 
-func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) {
-	detected := []*ReleaseSpec{}
-	for i := range st.Releases {
-		release := st.Releases[i]
+func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {
+	detected := []ReleaseSpec{}
+	for i := range releases {
+		release := releases[i]
 
 		if !release.Desired() {
 			installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
@@ -382,7 +393,7 @@ func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*Rele
 			} else if installed {
 				// Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554)
 				r := release
-				detected = append(detected, &r)
+				detected = append(detected, r)
 			}
 		}
 	}
@@ -399,105 +410,130 @@ func (o *SyncOpts) Apply(opts *SyncOpts) {
 	*opts = *o
 }
 
-// SyncReleases wrapper for executing helm upgrade on the releases
-func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int, opt ...SyncOpt) []error {
-	opts := &SyncOpts{}
-	for _, o := range opt {
-		o.Apply(opts)
-	}
+func ReleaseToID(r *ReleaseSpec) string {
+	var id string
 
-	preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit, opts)
-	if len(prepErrs) > 0 {
-		return prepErrs
+	tns := r.TillerNamespace
+	if tns != "" {
+		id += tns + "/"
 	}
 
-	availableIds := make([]string, len(preps))
-	idToPrep := map[string]syncPrepareResult{}
-
-	d := dag.New()
-	for i, p := range preps {
-		r := p.release
+	ns := r.Namespace
+	if ns != "" {
+		id += ns + "/"
+	}
 
-		id := releaseToID(r)
+	id += r.Name
 
-		idToPrep[id] = p
+	return id
+}
 
-		availableIds[i] = id
+// DeleteReleasesForSync deletes releases that are marked for deletion
+func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, helm helmexec.Interface, workerLimit int) []error {
+	errs := []error{}
 
-		d.Add(id, dag.Dependencies(r.Needs))
-	}
+	releases := st.Releases
 
-	plan, err := d.Plan()
-	if err != nil {
-		return []error{err}
+	jobQueue := make(chan *ReleaseSpec, len(releases))
+	results := make(chan syncResult, len(releases))
+	if workerLimit == 0 {
+		workerLimit = len(releases)
 	}
 
-	groupsTotal := len(plan)
-
-	st.logger.Debugf("syncing %d groups of releases in this order: %s", groupsTotal, plan)
+	m := new(sync.Mutex)
 
-	for id, prep := range idToPrep {
-		for _, need := range prep.release.Needs {
-			if _, ok := idToPrep[need]; !ok {
-				return []error{fmt.Errorf("%q needs %q, but it must be one of %s", id, need, strings.Join(availableIds, ", "))}
+	st.scatterGather(
+		workerLimit,
+		len(releases),
+		func() {
+			for i := 0; i < len(releases); i++ {
+				jobQueue <- &releases[i]
 			}
-		}
-	}
+			close(jobQueue)
+		},
+		func(workerIndex int) {
+			for release := range jobQueue {
+				var relErr *ReleaseError
+				context := st.createHelmContext(release, workerIndex)
 
-	for groupIndex, dagNodesInGroup := range plan {
-		var idsInGroup []string
-		var prepsInGroup []syncPrepareResult
+				if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
+					relErr = newReleaseError(release, err)
+				} else {
+					var args []string
+					if isHelm3() {
+						args = []string{}
+					} else {
+						args = []string{"--purge"}
+					}
+					deletionFlags := st.appendConnectionFlags(args, release)
+					m.Lock()
+					if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
+						affectedReleases.Failed = append(affectedReleases.Failed, release)
+						relErr = newReleaseError(release, err)
+					} else {
+						affectedReleases.Deleted = append(affectedReleases.Deleted, release)
+					}
+					m.Unlock()
+				}
 
-		for _, node := range dagNodesInGroup {
-			prepareResult, ok := idToPrep[node.Id]
-			if !ok {
-				panic(fmt.Sprintf("[bug] no release found for dag node id %q", node.Id))
-			}
-			prepsInGroup = append(prepsInGroup, prepareResult)
-			idsInGroup = append(idsInGroup, node.Id)
-		}
+				if relErr == nil {
+					results <- syncResult{}
+				} else {
+					results <- syncResult{errors: []*ReleaseError{relErr}}
+				}
 
-		st.logger.Debugf("syncing releases in group %d/%d: %s", groupIndex+1, groupsTotal, strings.Join(idsInGroup, ", "))
+				if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil {
+					st.logger.Warnf("warn: %v\n", err)
+				}
 
-		errs := st.syncReleaseGroup(affectedReleases, helm, workerLimit, prepsInGroup)
-		if len(errs) > 0 {
-			return errs
-		}
+				if _, err := st.triggerCleanupEvent(release, "sync"); err != nil {
+					st.logger.Warnf("warn: %v\n", err)
+				}
+			}
+		},
+		func() {
+			for i := 0; i < len(releases); {
+				select {
+				case res := <-results:
+					if len(res.errors) > 0 {
+						for _, e := range res.errors {
+							errs = append(errs, e)
+						}
+					}
+				}
+				i++
+			}
+		},
+	)
+	if len(errs) > 0 {
+		return errs
 	}
-
 	return nil
 }
 
-func releaseToID(r *ReleaseSpec) string {
-	var id string
-
-	tns := r.TillerNamespace
-	if tns != "" {
-		id += tns + "/"
+// SyncReleases wrapper for executing helm upgrade on the releases
+func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int, opt ...SyncOpt) []error {
+	opts := &SyncOpts{}
+	for _, o := range opt {
+		o.Apply(opts)
 	}
 
-	ns := r.Namespace
-	if ns != "" {
-		id += ns + "/"
+	preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit, opts)
+	if len(prepErrs) > 0 {
+		return prepErrs
 	}
 
-	id += r.Name
-
-	return id
-}
-
-func (st *HelmState) syncReleaseGroup(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, preps []syncPrepareResult) []error {
 	errs := []error{}
 	jobQueue := make(chan *syncPrepareResult, len(preps))
 	results := make(chan syncResult, len(preps))
-	if concurrency == 0 {
-		concurrency = len(preps)
+	if workerLimit == 0 {
+		workerLimit = len(preps)
 	}
 
 	m := new(sync.Mutex)
 
 	st.scatterGather(
-		concurrency,
+		workerLimit,
 		len(preps),
 		func() {
 			for i := 0; i < len(preps); i++ {
@@ -730,7 +766,7 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
 			continue
 		}
 
-		st.applyDefaultsTo(&release)
+		st.ApplyOverrides(&release)
 
 		flags, err := st.flagsForTemplate(helm, &release, 0)
 		if err != nil {
@@ -916,7 +952,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
 			for release := range jobs {
 				errs := []error{}
 
-				st.applyDefaultsTo(release)
+				st.ApplyOverrides(release)
 
 				// TODO We need a long-term fix for this :)
 				// See https://github.com/roboll/helmfile/issues/737
@@ -1020,7 +1056,7 @@ type DiffOpt interface{ Apply(*DiffOpts) }
 
 // DiffReleases wrapper for executing helm diff on the releases
 // It returns releases that had any changes
-func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool, opt ...DiffOpt) ([]*ReleaseSpec, []error) {
+func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool, opt ...DiffOpt) ([]ReleaseSpec, []error) {
 	opts := &DiffOpts{}
 	for _, o := range opt {
 		o.Apply(opts)
@@ -1028,13 +1064,13 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
 
 	preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets, opts)
 	if len(prepErrs) > 0 {
-		return []*ReleaseSpec{}, prepErrs
+		return []ReleaseSpec{}, prepErrs
 	}
 
 	jobQueue := make(chan *diffPrepareResult, len(preps))
 	results := make(chan diffResult, len(preps))
 
-	rs := []*ReleaseSpec{}
+	rs := []ReleaseSpec{}
 	errs := []error{}
 
 	st.scatterGather(
@@ -1076,7 +1112,7 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
 				if res.err != nil {
 					errs = append(errs, res.err)
 					if res.err.Code == 2 {
-						rs = append(rs, res.err.ReleaseSpec)
+						rs = append(rs, *res.err.ReleaseSpec)
 					}
 				}
 			}
@@ -1102,7 +1138,7 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [
 // DeleteReleases wrapper for executing helm delete on the releases
 // This function traverses the DAG of the releases in the reverse order, so that the releases that are NOT depended by any others are deleted first.
 func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool) []error {
-	return st.dagAwareReverseIterateOnReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
+	return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
 		if !release.Desired() {
 			return nil
 		}
@@ -1180,19 +1216,17 @@ func (st *HelmState) Clean() []error {
 	return nil
 }
 
-// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
-func (st *HelmState) FilterReleases() error {
-	var filteredReleases []ReleaseSpec
-	releaseSet := map[string][]ReleaseSpec{}
+func MarkFilteredReleases(releases []ReleaseSpec, selectors []string) ([]Release, error) {
+	var filteredReleases []Release
 	filters := []ReleaseFilter{}
-	for _, label := range st.Selectors {
+	for _, label := range selectors {
 		f, err := ParseLabels(label)
 		if err != nil {
-			return err
+			return nil, err
 		}
 		filters = append(filters, f)
 	}
-	for _, r := range st.Releases {
+	for _, r := range releases {
 		if r.Labels == nil {
 			r.Labels = map[string]string{}
 		}
@@ -1202,22 +1236,40 @@ func (st *HelmState) FilterReleases() error {
 		// Strip off just the last portion for the name stable/newrelic would give newrelic
 		chartSplit := strings.Split(r.Chart, "/")
 		r.Labels["chart"] = chartSplit[len(chartSplit)-1]
+		var matched bool
 		for _, f := range filters {
 			if r.Labels == nil {
 				r.Labels = map[string]string{}
 			}
 			if f.Match(r) {
-				releaseSet[r.Name] = append(releaseSet[r.Name], r)
-				continue
+				matched = true
+				break
 			}
 		}
+		res := Release{
+			ReleaseSpec: r,
+			Filtered:    len(filters) > 0 && !matched,
+		}
+		filteredReleases = append(filteredReleases, res)
+	}
+
+	return filteredReleases, nil
+}
+
+// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
+func (st *HelmState) FilterReleases() error {
+	filteredReleases, err := MarkFilteredReleases(st.Releases, st.Selectors)
+	if err != nil {
+		return err
 	}
-	for _, r := range releaseSet {
-		filteredReleases = append(filteredReleases, r...)
+	var releases []ReleaseSpec
+	for _, r := range filteredReleases {
+		if !r.Filtered {
+			releases = append(releases, r.ReleaseSpec)
+		}
 	}
-	st.Releases = filteredReleases
-	numFound := len(filteredReleases)
-	st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(st.Selectors, ","), st.FilePath)
+	st.Releases = releases
+	st.logger.Debugf("%d release(s) matching %s found in %s\n", len(filteredReleases), strings.Join(st.Selectors, ","), st.FilePath)
 	return nil
 }
 
@@ -1267,7 +1319,7 @@ func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpe
 		Hooks:         r.Hooks,
 		StateFilePath: st.FilePath,
 		BasePath:      st.basePath,
-		Namespace:     st.Namespace,
+		Namespace:     st.OverrideNamespace,
 		Env:           st.Env,
 		Logger:        st.logger,
 		ReadFile:      st.readFile,
diff --git a/pkg/state/state_exec_tmpl.go b/pkg/state/state_exec_tmpl.go
index 1d57085..c68ffbc 100644
--- a/pkg/state/state_exec_tmpl.go
+++ b/pkg/state/state_exec_tmpl.go
@@ -39,7 +39,7 @@ func (st *HelmState) mustLoadVals() map[string]interface{} {
 func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
 	return EnvironmentTemplateData{
 		Environment: st.Env,
-		Namespace:   st.Namespace,
+		Namespace:   st.OverrideNamespace,
 		Values:      st.mustLoadVals(),
 	}
 }
diff --git a/pkg/state/state_run.go b/pkg/state/state_run.go
index 9cbea83..9c7a2f5 100644
--- a/pkg/state/state_run.go
+++ b/pkg/state/state_run.go
@@ -2,7 +2,7 @@ package state
 
 import (
 	"fmt"
-	"strings"
+	"sort"
 	"sync"
 
 	"github.com/roboll/helmfile/pkg/helmexec"
@@ -104,51 +104,96 @@ func (st *HelmState) iterateOnReleases(helm helmexec.Interface, concurrency int,
 	return nil
 }
 
-func (st *HelmState) dagAwareReverseIterateOnReleases(helm helmexec.Interface, concurrency int,
-	do func(ReleaseSpec, int) error) []error {
+type PlanOpts struct {
+	Reverse bool
+}
+
+func PlanReleases(releases []ReleaseSpec, selectors []string, reverse bool) ([][]Release, error) {
+	marked, err := MarkFilteredReleases(releases, selectors)
+	if err != nil {
+		return nil, err
+	}
+
+	groups, err := SortedReleaseGroups(marked, reverse)
+	if err != nil {
+		return nil, err
+	}
 
-	idToRelease := map[string]ReleaseSpec{}
+	return groups, nil
+}
 
-	preps := st.Releases
+func SortedReleaseGroups(releases []Release, reverse bool) ([][]Release, error) {
+	groups, err := GroupReleasesByDependency(releases)
+	if err != nil {
+		return nil, err
+	}
+
+	if reverse {
+		sort.Slice(groups, func(i, j int) bool {
+			return j < i
+		})
+	}
+
+	return groups, nil
+}
+
+func GroupReleasesByDependency(releases []Release) ([][]Release, error) {
+	idToReleases := map[string][]Release{}
 
 	d := dag.New()
-	for _, r := range preps {
+	for _, r := range releases {
 
-		id := releaseToID(&r)
+		id := ReleaseToID(&r.ReleaseSpec)
 
-		idToRelease[id] = r
+		idToReleases[id] = append(idToReleases[id], r)
 
-		d.Add(id, dag.Dependencies(r.Needs))
+		// Only compute dependencies from non-filtered releases
+		if !r.Filtered {
+			d.Add(id, dag.Dependencies(r.Needs))
+		}
+	}
+
+	for _, r := range releases {
+		if !r.Filtered {
+			for _, n := range r.Needs {
+				if _, ok := idToReleases[n]; !ok {
+					id := ReleaseToID(&r.ReleaseSpec)
+					return nil, fmt.Errorf("%q has dependency to inexistent release %q", id, n)
+				}
+			}
+		}
 	}
 
 	plan, err := d.Plan()
 	if err != nil {
-		return []error{err}
+		return nil, err
 	}
 
-	groupsTotal := len(plan)
-
-	st.logger.Debugf("processing %d groups of releases in this order: %s", groupsTotal, plan)
+	var result [][]Release
 
-	for groupIndex := len(plan) - 1; groupIndex >= 0; groupIndex-- {
+	for groupIndex := 0; groupIndex < len(plan); groupIndex++ {
 		dagNodesInGroup := plan[groupIndex]
 
 		var idsInGroup []string
-		var releasesInGroup []ReleaseSpec
+		var releasesInGroup []Release
 
 		for _, node := range dagNodesInGroup {
-			releasesInGroup = append(releasesInGroup, idToRelease[node.Id])
 			idsInGroup = append(idsInGroup, node.Id)
 		}
 
-		st.logger.Debugf("processing releases in group %d/%d: %s", groupIndex+1, groupsTotal, strings.Join(idsInGroup, ", "))
-
-		errs := st.iterateOnReleases(helm, concurrency, releasesInGroup, do)
+		// Make the helmfile behavior deterministic for reproducibility and ease of testing
+		sort.Strings(idsInGroup)
 
-		if len(errs) > 0 {
-			return errs
+		for _, id := range idsInGroup {
+			releases, ok := idToReleases[id]
+			if !ok {
+				panic(fmt.Errorf("bug: unexpectedly failed to get releases for id %q", id))
+			}
+			releasesInGroup = append(releasesInGroup, releases...)
 		}
+
+		result = append(result, releasesInGroup)
 	}
 
-	return nil
+	return result, nil
 }


Test patch:
diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go
index 3fa45db..16d93e8 100644
--- a/pkg/app/app_test.go
+++ b/pkg/app/app_test.go
@@ -1,6 +1,7 @@
 package app
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
 	"io"
@@ -9,10 +10,12 @@ import (
 	"path/filepath"
 	"reflect"
 	"regexp"
+	"runtime"
 	"strings"
 	"sync"
 	"testing"
 
+	"github.com/roboll/helmfile/pkg/exectest"
 	"gotest.tools/assert"
 
 	"github.com/roboll/helmfile/pkg/helmexec"
@@ -339,6 +342,14 @@ releases:
 	}
 }
 
+type ctxLogger struct {
+	label string
+}
+
+func (cl *ctxLogger) Write(b []byte) (int, error) {
+	return os.Stderr.Write(append([]byte(cl.label+":"), b...))
+}
+
 // See https://github.com/roboll/helmfile/issues/322
 func TestVisitDesiredStatesWithReleasesFiltered_Selectors(t *testing.T) {
 	files := map[string]string{
@@ -399,39 +410,43 @@ releases:
 		{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
 	}
 
-	for _, testcase := range testcases {
-		actual := []string{}
+	for i := range testcases {
+		testcase := testcases[i]
 
-		collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
-			for _, r := range st.Releases {
-				actual = append(actual, r.Name)
+		t.Run(testcase.label, func(t *testing.T) {
+			actual := []string{}
+
+			collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
+				for _, r := range st.Releases {
+					actual = append(actual, r.Name)
+				}
+				return []error{}
 			}
-			return []error{}
-		}
 
-		app := appWithFs(&App{
-			KubeContext: "default",
-			Logger:      helmexec.NewLogger(os.Stderr, "debug"),
-			Namespace:   "",
-			Selectors:   []string{testcase.label},
-			Env:         "default",
-		}, files)
+			app := appWithFs(&App{
+				KubeContext: "default",
+				Logger:      helmexec.NewLogger(&ctxLogger{label: testcase.label}, "debug"),
+				Namespace:   "",
+				Selectors:   []string{testcase.label},
+				Env:         "default",
+			}, files)
 
-		err := app.VisitDesiredStatesWithReleasesFiltered(
-			"helmfile.yaml", collectReleases,
-		)
-		if testcase.expectErr {
-			if err == nil {
-				t.Errorf("error expected but not happened for selector %s", testcase.label)
-			} else if err.Error() != testcase.errMsg {
-				t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error())
+			err := app.VisitDesiredStatesWithReleasesFiltered(
+				"helmfile.yaml", collectReleases,
+			)
+			if testcase.expectErr {
+				if err == nil {
+					t.Errorf("error expected but not happened for selector %s", testcase.label)
+				} else if err.Error() != testcase.errMsg {
+					t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error())
+				}
+			} else if !testcase.expectErr && err != nil {
+				t.Errorf("unexpected error for selector %s: %v", testcase.label, err)
 			}
-		} else if !testcase.expectErr && err != nil {
-			t.Errorf("unexpected error for selector %s: %v", testcase.label, err)
-		}
-		if len(actual) != testcase.expectedCount {
-			t.Errorf("unexpected release count for selector %s: expected=%d, actual=%d", testcase.label, testcase.expectedCount, len(actual))
-		}
+			if len(actual) != testcase.expectedCount {
+				t.Errorf("unexpected release count for selector %s: expected=%d, actual=%d", testcase.label, testcase.expectedCount, len(actual))
+			}
+		})
 	}
 }
 
@@ -1838,6 +1853,59 @@ func (c configImpl) Concurrency() int {
 	return 1
 }
 
+type applyConfig struct {
+	args            string
+	values          []string
+	set             []string
+	skipDeps        bool
+	suppressSecrets bool
+	noColor         bool
+	context         int
+	concurrency     int
+	interactive     bool
+	logger          *zap.SugaredLogger
+}
+
+func (a applyConfig) Args() string {
+	return a.args
+}
+
+func (a applyConfig) Values() []string {
+	return a.values
+}
+
+func (a applyConfig) Set() []string {
+	return a.set
+}
+
+func (a applyConfig) SkipDeps() bool {
+	return a.skipDeps
+}
+
+func (a applyConfig) SuppressSecrets() bool {
+	return a.suppressSecrets
+}
+
+func (a applyConfig) NoColor() bool {
+	return a.noColor
+}
+
+func (a applyConfig) Context() int {
+	return a.context
+}
+
+func (a applyConfig) Concurrency() int {
+	return a.concurrency
+}
+
+func (a applyConfig) Interactive() bool {
+	return a.interactive
+}
+
+func (a applyConfig) Logger() *zap.SugaredLogger {
+	return a.logger
+}
+
 // Mocking the command-line runner
 
 type mockRunner struct {
@@ -1850,7 +1918,7 @@ func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string
 }
 
 func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface {
-	execer := helmexec.New(logger, kubeContext, &mockRunner{})
+	execer := helmexec.New("helm", logger, kubeContext, &mockRunner{})
 	return execer
 }
 
@@ -1909,9 +1977,11 @@ func (helm *mockHelmExec) ReleaseStatus(context helmexec.HelmContext, release st
 func (helm *mockHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
 	return nil
 }
+
 func (helm *mockHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
 	return "", nil
 }
+
 func (helm *mockHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
 	return "", nil
 }
@@ -1983,6 +2053,1110 @@ releases:
 	}
 }
 
+func TestApply(t *testing.T) {
+	testcases := []struct {
+		name        string
+		loc         string
+		ns          string
+		concurrency int
+		error       string
+		files       map[string]string
+		lists       map[exectest.ListKey]string
+		diffs       map[exectest.DiffKey]error
+		upgraded    []exectest.Release
+		deleted     []exectest.Release
+		log         string
+	}{
+		//
+		// complex test cases for smoke testing
+		//
+		{
+			name: "smoke",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: database
+  chart: charts/mysql
+  needs:
+  - logging
+- name: frontend-v1
+  chart: charts/frontend
+  installed: false
+  needs:
+  - servicemesh
+  - logging
+  - backend-v1
+- name: frontend-v2
+  chart: charts/frontend
+  needs:
+  - servicemesh
+  - logging
+  - backend-v2
+- name: frontend-v3
+  chart: charts/frontend
+  needs:
+  - servicemesh
+  - logging
+  - backend-v2
+- name: backend-v1
+  chart: charts/backend
+  installed: false
+  needs:
+  - servicemesh
+  - logging
+  - database
+  - anotherbackend
+- name: backend-v2
+  chart: charts/backend
+  needs:
+  - servicemesh
+  - logging
+  - database
+  - anotherbackend
+- name: anotherbackend
+  chart: charts/anotherbackend
+  needs:
+  - servicemesh
+  - logging
+  - database
+- name: servicemesh
+  chart: charts/istio
+  needs:
+  - logging
+- name: logging
+  chart: charts/fluent-bit
+- name: front-proxy
+  chart: stable/envoy
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				// noop on frontend-v2
+				exectest.DiffKey{Name: "frontend-v2", Chart: "charts/frontend", Flags: "--kube-contextdefault--detailed-exitcode"}: nil,
+				// install frontend-v3
+				exectest.DiffKey{Name: "frontend-v3", Chart: "charts/frontend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				// upgrades
+				exectest.DiffKey{Name: "logging", Chart: "charts/fluent-bit", Flags: "--kube-contextdefault--detailed-exitcode"}:            helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "front-proxy", Chart: "stable/envoy", Flags: "--kube-contextdefault--detailed-exitcode"}:             helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "servicemesh", Chart: "charts/istio", Flags: "--kube-contextdefault--detailed-exitcode"}:             helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "database", Chart: "charts/mysql", Flags: "--kube-contextdefault--detailed-exitcode"}:                helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "backend-v2", Chart: "charts/backend", Flags: "--kube-contextdefault--detailed-exitcode"}:            helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "anotherbackend", Chart: "charts/anotherbackend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				// delete frontend-v1 and backend-v1
+				exectest.ListKey{Filter: "^frontend-v1$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+frontend-v1 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	backend-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^backend-v1$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+backend-v1 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	backend-3.1.0	3.1.0      	default
+`,
+			},
+			// Disable concurrency to avoid in-deterministic result
+			concurrency: 1,
+			upgraded: []exectest.Release{
+				{Name: "front-proxy", Flags: []string{}},
+				{Name: "logging", Flags: []string{}},
+				{Name: "database", Flags: []string{}},
+				{Name: "servicemesh", Flags: []string{}},
+				{Name: "anotherbackend", Flags: []string{}},
+				{Name: "backend-v2", Flags: []string{}},
+				{Name: "frontend-v3", Flags: []string{}},
+			},
+			deleted: []exectest.Release{
+				{Name: "frontend-v1", Flags: []string{}},
+			},
+			log: `processing file "helmfile.yaml" in directory "."
+first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
+first-pass uses: &{default map[] map[]}
+first-pass rendering output of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: database
+ 3:   chart: charts/mysql
+ 4:   needs:
+ 5:   - logging
+ 6: - name: frontend-v1
+ 7:   chart: charts/frontend
+ 8:   installed: false
+ 9:   needs:
+10:   - servicemesh
+11:   - logging
+12:   - backend-v1
+13: - name: frontend-v2
+14:   chart: charts/frontend
+15:   needs:
+16:   - servicemesh
+17:   - logging
+18:   - backend-v2
+19: - name: frontend-v3
+20:   chart: charts/frontend
+21:   needs:
+22:   - servicemesh
+23:   - logging
+24:   - backend-v2
+25: - name: backend-v1
+26:   chart: charts/backend
+27:   installed: false
+28:   needs:
+29:   - servicemesh
+30:   - logging
+31:   - database
+32:   - anotherbackend
+33: - name: backend-v2
+34:   chart: charts/backend
+35:   needs:
+36:   - servicemesh
+37:   - logging
+38:   - database
+39:   - anotherbackend
+40: - name: anotherbackend
+41:   chart: charts/anotherbackend
+42:   needs:
+43:   - servicemesh
+44:   - logging
+45:   - database
+46: - name: servicemesh
+47:   chart: charts/istio
+48:   needs:
+49:   - logging
+50: - name: logging
+51:   chart: charts/fluent-bit
+52: - name: front-proxy
+53:   chart: stable/envoy
+54: 
+
+first-pass produced: &{default map[] map[]}
+first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
+vals:
+map[]
+defaultVals:[]
+second-pass rendering result of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: database
+ 3:   chart: charts/mysql
+ 4:   needs:
+ 5:   - logging
+ 6: - name: frontend-v1
+ 7:   chart: charts/frontend
+ 8:   installed: false
+ 9:   needs:
+10:   - servicemesh
+11:   - logging
+12:   - backend-v1
+13: - name: frontend-v2
+14:   chart: charts/frontend
+15:   needs:
+16:   - servicemesh
+17:   - logging
+18:   - backend-v2
+19: - name: frontend-v3
+20:   chart: charts/frontend
+21:   needs:
+22:   - servicemesh
+23:   - logging
+24:   - backend-v2
+25: - name: backend-v1
+26:   chart: charts/backend
+27:   installed: false
+28:   needs:
+29:   - servicemesh
+30:   - logging
+31:   - database
+32:   - anotherbackend
+33: - name: backend-v2
+34:   chart: charts/backend
+35:   needs:
+36:   - servicemesh
+37:   - logging
+38:   - database
+39:   - anotherbackend
+40: - name: anotherbackend
+41:   chart: charts/anotherbackend
+42:   needs:
+43:   - servicemesh
+44:   - logging
+45:   - database
+46: - name: servicemesh
+47:   chart: charts/istio
+48:   needs:
+49:   - logging
+50: - name: logging
+51:   chart: charts/fluent-bit
+52: - name: front-proxy
+53:   chart: stable/envoy
+54: 
+
+merged environment: &{default map[] map[]}
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+worker 1/1 finished
+Affected releases are:
+  anotherbackend (charts/anotherbackend) UPDATED
+  backend-v1 (charts/backend) DELETED
+  backend-v2 (charts/backend) UPDATED
+  database (charts/mysql) UPDATED
+  front-proxy (stable/envoy) UPDATED
+  frontend-v1 (charts/frontend) DELETED
+  frontend-v3 (charts/frontend) UPDATED
+  logging (charts/fluent-bit) UPDATED
+  servicemesh (charts/istio) UPDATED
+
+processing 5 groups of releases in this order:
+GROUP RELEASES
+1     frontend-v1, frontend-v2, frontend-v3
+2     backend-v1, backend-v2
+3     anotherbackend
+4     database, servicemesh
+5     front-proxy, logging
+
+processing releases in group 1/5: frontend-v1, frontend-v2, frontend-v3
+worker 1/1 started
+worker 1/1 finished
+processing releases in group 2/5: backend-v1, backend-v2
+worker 1/1 started
+worker 1/1 finished
+processing releases in group 3/5: anotherbackend
+processing releases in group 4/5: database, servicemesh
+processing releases in group 5/5: front-proxy, logging
+processing 5 groups of releases in this order:
+GROUP RELEASES
+1     front-proxy, logging
+2     database, servicemesh
+3     anotherbackend
+4     backend-v1, backend-v2
+5     frontend-v1, frontend-v2, frontend-v3
+
+processing releases in group 1/5: front-proxy, logging
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^front-proxy$ --kube-contextdefault}
+getting deployed release version failed:unexpected list key: {^logging$ --kube-contextdefault}
+worker 1/1 finished
+processing releases in group 2/5: database, servicemesh
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^database$ --kube-contextdefault}
+getting deployed release version failed:unexpected list key: {^servicemesh$ --kube-contextdefault}
+worker 1/1 finished
+processing releases in group 3/5: anotherbackend
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^anotherbackend$ --kube-contextdefault}
+worker 1/1 finished
+processing releases in group 4/5: backend-v1, backend-v2
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^backend-v2$ --kube-contextdefault}
+worker 1/1 finished
+processing releases in group 5/5: frontend-v1, frontend-v2, frontend-v3
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^frontend-v3$ --kube-contextdefault}
+worker 1/1 finished
+`,
+		},
+		//
+		// noop: no changes
+		//
+		{
+			name: "noop",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+- name: foo
+  chart: mychart1
+  installed: false
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: nil,
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: ``,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{},
+			deleted:  []exectest.Release{},
+		},
+		//
+		// install
+		//
+		{
+			name: "install",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: baz
+  chart: mychart3
+- name: foo
+  chart: mychart1
+  needs:
+  - bar
+- name: bar
+  chart: mychart2
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "baz", Chart: "mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+				{Name: "baz", Flags: []string{}},
+				{Name: "foo", Flags: []string{}},
+			},
+			deleted:     []exectest.Release{},
+			concurrency: 1,
+			log: `processing file "helmfile.yaml" in directory "."
+first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
+first-pass uses: &{default map[] map[]}
+first-pass rendering output of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   chart: mychart3
+ 4: - name: foo
+ 5:   chart: mychart1
+ 6:   needs:
+ 7:   - bar
+ 8: - name: bar
+ 9:   chart: mychart2
+10: 
+
+first-pass produced: &{default map[] map[]}
+first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
+vals:
+map[]
+defaultVals:[]
+second-pass rendering result of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   chart: mychart3
+ 4: - name: foo
+ 5:   chart: mychart1
+ 6:   needs:
+ 7:   - bar
+ 8: - name: bar
+ 9:   chart: mychart2
+10: 
+
+merged environment: &{default map[] map[]}
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+worker 1/1 finished
+Affected releases are:
+  bar (mychart2) UPDATED
+  baz (mychart3) UPDATED
+  foo (mychart1) UPDATED
+
+processing 2 groups of releases in this order:
+GROUP RELEASES
+1     bar, baz
+2     foo
+
+processing releases in group 1/2: bar, baz
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^bar$ --kube-contextdefault}
+getting deployed release version failed:unexpected list key: {^baz$ --kube-contextdefault}
+worker 1/1 finished
+processing releases in group 2/2: foo
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^foo$ --kube-contextdefault}
+worker 1/1 finished
+`,
+		},
+		//
+		// upgrades
+		//
+		{
+			name: "upgrade when foo needs bar",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+- name: foo
+  chart: mychart1
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+				{Name: "foo", Flags: []string{}},
+			},
+		},
+		{
+			name: "upgrade when bar needs foo",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: foo
+  chart: mychart1
+- name: bar
+  chart: mychart2
+  needs:
+  - foo
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+				{Name: "bar", Flags: []string{}},
+			},
+		},
+		{
+			name: "upgrade when foo needs bar, with ns override",
+			loc:  location(),
+			ns:   "testNamespace",
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+- name: foo
+  chart: mychart1
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+				{Name: "foo", Flags: []string{}},
+			},
+		},
+		{
+			name: "upgrade when bar needs foo, with ns override",
+			loc:  location(),
+			ns:   "testNamespace",
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: foo
+  chart: mychart1
+- name: bar
+  chart: mychart2
+  needs:
+  - foo
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+				{Name: "bar", Flags: []string{}},
+			},
+		},
+		{
+			name: "upgrade when ns1/foo needs ns2/bar",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: foo
+  chart: mychart1
+  namespace: ns1
+  needs:
+  - ns2/bar
+- name: bar
+  chart: mychart2
+  namespace: ns2
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{"--kube-context", "default", "--namespace", "ns2"}},
+				{Name: "foo", Flags: []string{"--kube-context", "default", "--namespace", "ns1"}},
+			},
+		},
+		{
+			name: "upgrade when ns2/bar needs ns1/foo",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+  namespace: ns2
+  needs:
+  - ns1/foo
+- name: foo
+  chart: mychart1
+  namespace: ns1
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "foo", Flags: []string{"--kube-context", "default", "--namespace", "ns1"}},
+				{Name: "bar", Flags: []string{"--kube-context", "default", "--namespace", "ns2"}},
+			},
+		},
+		{
+			name: "upgrade when tns1/ns1/foo needs tns2/ns2/bar",
+			loc:  location(),
+
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: foo
+  chart: mychart1
+  namespace: ns1
+  tillerNamespace: tns1
+  needs:
+  - tns2/ns2/bar
+- name: bar
+  chart: mychart2
+  namespace: ns2
+  tillerNamespace: tns2
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--tiller-namespacetns2--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--tiller-namespacetns1--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{"--tiller-namespace", "tns2", "--kube-context", "default", "--namespace", "ns2"}},
+				{Name: "foo", Flags: []string{"--tiller-namespace", "tns1", "--kube-context", "default", "--namespace", "ns1"}},
+			},
+		},
+		{
+			name: "upgrade when tns2/ns2/bar needs tns1/ns1/foo",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+  namespace: ns2
+  tillerNamespace: tns2
+  needs:
+  - tns1/ns1/foo
+- name: foo
+  chart: mychart1
+  namespace: ns1
+  tillerNamespace: tns1
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--tiller-namespacetns2--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--tiller-namespacetns1--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			upgraded: []exectest.Release{
+				{Name: "foo", Flags: []string{"--tiller-namespace", "tns1", "--kube-context", "default", "--namespace", "ns1"}},
+				{Name: "bar", Flags: []string{"--tiller-namespace", "tns2", "--kube-context", "default", "--namespace", "ns2"}},
+			},
+			// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
+			concurrency: 1,
+			log: `processing file "helmfile.yaml" in directory "."
+first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
+first-pass uses: &{default map[] map[]}
+first-pass rendering output of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: bar
+ 3:   chart: mychart2
+ 4:   namespace: ns2
+ 5:   tillerNamespace: tns2
+ 6:   needs:
+ 7:   - tns1/ns1/foo
+ 8: - name: foo
+ 9:   chart: mychart1
+10:   namespace: ns1
+11:   tillerNamespace: tns1
+12: 
+
+first-pass produced: &{default map[] map[]}
+first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
+vals:
+map[]
+defaultVals:[]
+second-pass rendering result of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: bar
+ 3:   chart: mychart2
+ 4:   namespace: ns2
+ 5:   tillerNamespace: tns2
+ 6:   needs:
+ 7:   - tns1/ns1/foo
+ 8: - name: foo
+ 9:   chart: mychart1
+10:   namespace: ns1
+11:   tillerNamespace: tns1
+12: 
+
+merged environment: &{default map[] map[]}
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+worker 1/1 finished
+Affected releases are:
+  bar (mychart2) UPDATED
+  foo (mychart1) UPDATED
+
+processing 2 groups of releases in this order:
+GROUP RELEASES
+1     tns1/ns1/foo
+2     tns2/ns2/bar
+
+processing releases in group 1/2: tns1/ns1/foo
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^foo$ --tiller-namespacetns1--kube-contextdefault}
+worker 1/1 finished
+processing releases in group 2/2: tns2/ns2/bar
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+getting deployed release version failed:unexpected list key: {^bar$ --tiller-namespacetns2--kube-contextdefault}
+worker 1/1 finished
+`,
+		},
+		//
+		// deletes: deleting all releases in the correct order
+		//
+		{
+			name: "delete foo and bar when foo needs bar",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+  installed: false
+- name: foo
+  chart: mychart1
+  installed: false
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+foo 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart1-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			deleted: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+				{Name: "bar", Flags: []string{}},
+			},
+		},
+		{
+			name: "delete foo and bar when bar needs foo",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+  installed: false
+  needs:
+  - foo
+- name: foo
+  chart: mychart1
+  installed: false
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+foo 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart1-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			deleted: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+				{Name: "foo", Flags: []string{}},
+			},
+		},
+		//
+		// upgrade and delete: upgrading one while deleting another
+		//
+		{
+			name: "delete foo when foo needs bar",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+- name: foo
+  chart: mychart1
+  installed: false
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+foo 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart1-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+			},
+			deleted: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+			},
+		},
+		{
+			name: "delete bar when foo needs bar",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: bar
+  chart: mychart2
+  installed: false
+- name: foo
+  chart: mychart1
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+foo 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart1-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+			},
+			deleted: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+			},
+		},
+		{
+			name: "delete foo when bar needs foo",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: foo
+  chart: mychart1
+  installed: false
+- name: bar
+  chart: mychart2
+  needs:
+  - foo
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+foo 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart1-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+			},
+			deleted: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+			},
+		},
+		{
+			name: "delete bar when bar needs foo",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: foo
+  chart: mychart1
+- name: bar
+  chart: mychart2
+  installed: false
+  needs:
+  - foo
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+foo 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart1-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{
+				{Name: "foo", Flags: []string{}},
+			},
+			deleted: []exectest.Release{
+				{Name: "bar", Flags: []string{}},
+			},
+		},
+		//
+		// error cases
+		//
+		{
+			name: "non-existent release in needs",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: baz
+  namespace: ns1
+  chart: mychart3
+- name: foo
+  chart: mychart1
+  needs:
+  - bar
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "baz", Chart: "mychart3", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}:               helmexec.ExitError{Code: 2},
+			},
+			lists:       map[exectest.ListKey]string{},
+			upgraded:    []exectest.Release{},
+			deleted:     []exectest.Release{},
+			concurrency: 1,
+			error:       `in ./helmfile.yaml: "foo" has dependency to inexistent release "bar"`,
+			log: `processing file "helmfile.yaml" in directory "."
+first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
+first-pass uses: &{default map[] map[]}
+first-pass rendering output of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   namespace: ns1
+ 4:   chart: mychart3
+ 5: - name: foo
+ 6:   chart: mychart1
+ 7:   needs:
+ 8:   - bar
+ 9: 
+
+first-pass produced: &{default map[] map[]}
+first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
+vals:
+map[]
+defaultVals:[]
+second-pass rendering result of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   namespace: ns1
+ 4:   chart: mychart3
+ 5: - name: foo
+ 6:   chart: mychart1
+ 7:   needs:
+ 8:   - bar
+ 9: 
+
+merged environment: &{default map[] map[]}
+worker 1/1 started
+worker 1/1 finished
+worker 1/1 started
+worker 1/1 finished
+Affected releases are:
+  baz (mychart3) UPDATED
+  foo (mychart1) UPDATED
+
+err: "foo" has dependency to inexistent release "bar"
+`,
+		},
+	}
+
+	for i := range testcases {
+		tc := testcases[i]
+		t.Run(tc.name, func(t *testing.T) {
+			wantUpgrades := tc.upgraded
+			wantDeletes := tc.deleted
+
+			var helm = &exectest.Helm{
+				FailOnUnexpectedList: true,
+				FailOnUnexpectedDiff: true,
+				Lists:                tc.lists,
+				Diffs:                tc.diffs,
+				DiffMutex:            &sync.Mutex{},
+				ChartsMutex:          &sync.Mutex{},
+				ReleasesMutex:        &sync.Mutex{},
+			}
+
+			bs := &bytes.Buffer{}
+
+			func() {
+				logReader, logWriter := io.Pipe()
+
+				logFlushed := &sync.WaitGroup{}
+				// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
+				logFlushed.Add(1)
+				go func() {
+					scanner := bufio.NewScanner(logReader)
+					for scanner.Scan() {
+						bs.Write(scanner.Bytes())
+						bs.WriteString("\n")
+					}
+					logFlushed.Done()
+				}()
+
+				defer func() {
+					// This is here to avoid data-trace on bytes buffer `bs` to capture logs
+					if err := logWriter.Close(); err != nil {
+						panic(err)
+					}
+					logFlushed.Wait()
+				}()
+
+				logger := helmexec.NewLogger(logWriter, "debug")
+
+				valsRuntime, err := vals.New(32)
+				if err != nil {
+					t.Errorf("unexpected error creating vals runtime: %v", err)
+				}
+
+				app := appWithFs(&App{
+					glob:        filepath.Glob,
+					abs:         filepath.Abs,
+					KubeContext: "default",
+					Env:         "default",
+					Logger:      logger,
+					helmExecer:  helm,
+					valsRuntime: valsRuntime,
+				}, tc.files)
+
+				if tc.ns != "" {
+					app.Namespace = tc.ns
+				}
+
+				applyErr := app.Apply(applyConfig{
+					// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
+					concurrency: tc.concurrency,
+					logger:      logger,
+				})
+				if tc.error == "" && applyErr != nil {
+					t.Fatalf("unexpected error for data defined at %s: %v", tc.loc, applyErr)
+				} else if tc.error != "" && applyErr == nil {
+					t.Fatalf("expected error did not occur for data defined at %s", tc.loc)
+				} else if tc.error != "" && applyErr != nil && tc.error != applyErr.Error() {
+					t.Fatalf("invalid error: expected %q, got %q", tc.error, applyErr.Error())
+				}
+
+				if len(wantUpgrades) > len(helm.Releases) {
+					t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
+				}
+
+				for relIdx := range wantUpgrades {
+					if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
+						t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
+					}
+					for flagIdx := range wantUpgrades[relIdx].Flags {
+						if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
+							t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
+						}
+					}
+				}
+
+				if len(wantDeletes) > len(helm.Deleted) {
+					t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
+				}
+
+				for relIdx := range wantDeletes {
+					if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name {
+						t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name)
+					}
+					for flagIdx := range wantDeletes[relIdx].Flags {
+						if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
+							t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
+						}
+					}
+				}
+			}()
+
+			if tc.log != "" {
+				actual := bs.String()
+
+				diff, exists := testhelper.Diff(tc.log, actual, 3)
+				if exists {
+					t.Errorf("unexpected log for data defined %s:\nDIFF\n%s\nEOD", tc.loc, diff)
+				}
+			}
+		})
+	}
+}
+
 func captureStdout(f func()) string {
 	reader, writer, err := os.Pipe()
 	if err != nil {
@@ -2139,3 +3313,8 @@ myrelease4	         	true     	id:myrelease1
 `
 	assert.Equal(t, expected, out)
 }
+
+func location() string {
+	_, fn, line, _ := runtime.Caller(1)
+	return fmt.Sprintf("%s:%d", filepath.Base(fn), line)
+}
diff --git a/pkg/exectest/helm.go b/pkg/exectest/helm.go
new file mode 100644
index 0000000..ebcf1fa
--- /dev/null
+++ b/pkg/exectest/helm.go
@@ -0,0 +1,167 @@
+package exectest
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"sync"
+
+	"github.com/roboll/helmfile/pkg/helmexec"
+)
+
+type ListKey struct {
+	Filter string
+	Flags  string
+}
+
+type DiffKey struct {
+	Name  string
+	Chart string
+	Flags string
+}
+
+type Helm struct {
+	Charts               []string
+	Repo                 []string
+	Releases             []Release
+	Deleted              []Release
+	Lists                map[ListKey]string
+	Diffs                map[DiffKey]error
+	Diffed               []Release
+	FailOnUnexpectedDiff bool
+	FailOnUnexpectedList bool
+
+	UpdateDepsCallbacks map[string]func(string) error
+
+	DiffMutex     *sync.Mutex
+	ChartsMutex   *sync.Mutex
+	ReleasesMutex *sync.Mutex
+}
+
+type Release struct {
+	Name  string
+	Flags []string
+}
+
+type Affected struct {
+	Upgraded []*Release
+	Deleted  []*Release
+	Failed   []*Release
+}
+
+func (helm *Helm) UpdateDeps(chart string) error {
+	if strings.Contains(chart, "error") {
+		return fmt.Errorf("simulated UpdateDeps failure for chart: %s", chart)
+	}
+	helm.Charts = append(helm.Charts, chart)
+
+	if helm.UpdateDepsCallbacks != nil {
+		callback, exists := helm.UpdateDepsCallbacks[chart]
+		if exists {
+			if err := callback(chart); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (helm *Helm) BuildDeps(name, chart string) error {
+	if strings.Contains(chart, "error") {
+		return errors.New("error")
+	}
+	helm.Charts = append(helm.Charts, chart)
+	return nil
+}
+
+func (helm *Helm) SetExtraArgs(args ...string) {
+	return
+}
+func (helm *Helm) SetHelmBinary(bin string) {
+	return
+}
+func (helm *Helm) AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error {
+	helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password}
+	return nil
+}
+func (helm *Helm) UpdateRepo() error {
+	return nil
+}
+func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
+	if strings.Contains(name, "error") {
+		return errors.New("error")
+	}
+	helm.sync(helm.ReleasesMutex, func() {
+		helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags})
+	})
+	helm.sync(helm.ChartsMutex, func() {
+		helm.Charts = append(helm.Charts, chart)
+	})
+
+	return nil
+}
+func (helm *Helm) DiffRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
+	if helm.DiffMutex != nil {
+		helm.DiffMutex.Lock()
+	}
+	helm.Diffed = append(helm.Diffed, Release{Name: name, Flags: flags})
+	if helm.DiffMutex != nil {
+		helm.DiffMutex.Unlock()
+	}
+	key := DiffKey{Name: name, Chart: chart, Flags: strings.Join(flags, "")}
+	err, ok := helm.Diffs[key]
+	if !ok && helm.FailOnUnexpectedDiff {
+		return fmt.Errorf("unexpected diff with key: %v", key)
+	}
+	return err
+}
+func (helm *Helm) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
+	if strings.Contains(release, "error") {
+		return errors.New("error")
+	}
+	helm.Releases = append(helm.Releases, Release{Name: release, Flags: flags})
+	return nil
+}
+func (helm *Helm) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
+	if strings.Contains(name, "error") {
+		return errors.New("error")
+	}
+	helm.Deleted = append(helm.Deleted, Release{Name: name, Flags: flags})
+	return nil
+}
+func (helm *Helm) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
+	key := ListKey{Filter: filter, Flags: strings.Join(flags, "")}
+	res, ok := helm.Lists[key]
+	if !ok && helm.FailOnUnexpectedList {
+		return "", fmt.Errorf("unexpected list key: %v", key)
+	}
+	return res, nil
+}
+func (helm *Helm) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
+	return "", nil
+}
+func (helm *Helm) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
+	if strings.Contains(name, "error") {
+		return errors.New("error")
+	}
+	helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags})
+	return nil
+}
+func (helm *Helm) Fetch(chart string, flags ...string) error {
+	return nil
+}
+func (helm *Helm) Lint(name, chart string, flags ...string) error {
+	return nil
+}
+func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error {
+	return nil
+}
+
+func (helm *Helm) sync(m *sync.Mutex, f func()) {
+	if m != nil {
+		m.Lock()
+		defer m.Unlock()
+	}
+
+	f()
+}
diff --git a/pkg/helmexec/exec_test.go b/pkg/helmexec/exec_test.go
index f0d2241..0bb32ff 100644
--- a/pkg/helmexec/exec_test.go
+++ b/pkg/helmexec/exec_test.go
@@ -24,7 +24,7 @@ func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string
 }
 
 func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
-	execer := New(logger, kubeContext, &mockRunner{})
+	execer := New("helm", logger, kubeContext, &mockRunner{})
 	return execer
 }
 
@@ -33,7 +33,7 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
 func TestNewHelmExec(t *testing.T) {
 	buffer := bytes.NewBufferString("something")
 	logger := NewLogger(buffer, "debug")
-	helm := New(logger, "dev", &ShellRunner{
+	helm := New("helm", logger, "dev", &ShellRunner{
 		Logger: logger,
 	})
 	if helm.kubeContext != "dev" {
@@ -50,7 +50,7 @@ func TestNewHelmExec(t *testing.T) {
 func Test_SetExtraArgs(t *testing.T) {
 	buffer := bytes.NewBufferString("something")
 	logger := NewLogger(buffer, "debug")
-	helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
+	helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
 		Logger: logger,
 	})
 	helm.SetExtraArgs()
@@ -70,7 +70,7 @@ func Test_SetExtraArgs(t *testing.T) {
 func Test_SetHelmBinary(t *testing.T) {
 	buffer := bytes.NewBufferString("something")
 	logger := NewLogger(buffer, "debug")
-	helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
+	helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
 		Logger: logger,
 	})
 	if helm.helmBinary != "helm" {
diff --git a/pkg/state/state_exec_tmpl_test.go b/pkg/state/state_exec_tmpl_test.go
index 05aa00f..7ba7aca 100644
--- a/pkg/state/state_exec_tmpl_test.go
+++ b/pkg/state/state_exec_tmpl_test.go
@@ -138,9 +138,9 @@ func TestHelmState_executeTemplates(t *testing.T) {
 				HelmDefaults: HelmSpec{
 					KubeContext: "test_context",
 				},
-				Env:          environment.Environment{Name: "test_env"},
-				Namespace:    "test-namespace_",
-				Repositories: nil,
+				Env:               environment.Environment{Name: "test_env"},
+				OverrideNamespace: "test-namespace_",
+				Repositories:      nil,
 				Releases: []ReleaseSpec{
 					tt.input,
 				},
@@ -238,9 +238,9 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
 				HelmDefaults: HelmSpec{
 					KubeContext: "test_context",
 				},
-				Env:          environment.Environment{Name: "test_env"},
-				Namespace:    "test-namespace_",
-				Repositories: nil,
+				Env:               environment.Environment{Name: "test_env"},
+				OverrideNamespace: "test-namespace_",
+				Repositories:      nil,
 				Releases: []ReleaseSpec{
 					tt.input,
 				},
diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go
index 786acb0..8c2ad54 100644
--- a/pkg/state/state_test.go
+++ b/pkg/state/state_test.go
@@ -1,20 +1,17 @@
 package state
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"reflect"
 	"testing"
 
+	"github.com/roboll/helmfile/pkg/exectest"
 	"github.com/roboll/helmfile/pkg/helmexec"
 	"github.com/roboll/helmfile/pkg/testhelper"
 	"github.com/variantdev/vals"
-
-	"errors"
-	"strings"
-
-	"fmt"
 )
 
 var logger = helmexec.NewLogger(os.Stdout, "warn")
@@ -133,12 +130,12 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
 				basePath:           tt.fields.BaseChartPath,
 				DeprecatedContext:  tt.fields.Context,
 				DeprecatedReleases: tt.fields.DeprecatedReleases,
-				Namespace:          tt.fields.Namespace,
+				OverrideNamespace:  tt.fields.Namespace,
 				Repositories:       tt.fields.Repositories,
 				Releases:           tt.fields.Releases,
 			}
-			if state.applyDefaultsTo(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
-				t.Errorf("HelmState.applyDefaultsTo() = %v, want %v", tt.args.spec, tt.want)
+			if state.ApplyOverrides(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
+				t.Errorf("HelmState.ApplyOverrides() = %v, want %v", tt.args.spec, tt.want)
 			}
 		})
 	}
@@ -527,7 +524,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
 				HelmDefaults:      tt.defaults,
 				valsRuntime:       valsRuntime,
 			}
-			helm := helmexec.New(logger, "default", &helmexec.ShellRunner{
+			helm := helmexec.New("helm", logger, "default", &helmexec.ShellRunner{
 				Logger: logger,
 			})
 			args, err := state.flagsForUpgrade(helm, tt.release, 0)
@@ -665,124 +662,11 @@ func Test_normalizeChart(t *testing.T) {
 
 // mocking helmexec.Interface
 
-type listKey struct {
-	filter string
-	flags  string
-}
-
-type mockHelmExec struct {
-	charts   []string
-	repo     []string
-	releases []mockRelease
-	deleted  []mockRelease
-	lists    map[listKey]string
-	diffed   []mockRelease
-
-	updateDepsCallbacks map[string]func(string) error
-}
-
-type mockRelease struct {
-	name  string
-	flags []string
-}
-
-type mockAffected struct {
-	upgraded []*mockRelease
-	deleted  []*mockRelease
-	failed   []*mockRelease
-}
-
-func (helm *mockHelmExec) UpdateDeps(chart string) error {
-	if strings.Contains(chart, "error") {
-		return fmt.Errorf("simulated UpdateDeps failure for chart: %s", chart)
-	}
-	helm.charts = append(helm.charts, chart)
-
-	if helm.updateDepsCallbacks != nil {
-		callback, exists := helm.updateDepsCallbacks[chart]
-		if exists {
-			if err := callback(chart); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func (helm *mockHelmExec) BuildDeps(name, chart string) error {
-	if strings.Contains(chart, "error") {
-		return errors.New("error")
-	}
-	helm.charts = append(helm.charts, chart)
-	return nil
-}
-
-func (helm *mockHelmExec) SetExtraArgs(args ...string) {
-	return
-}
-func (helm *mockHelmExec) SetHelmBinary(bin string) {
-	return
-}
-func (helm *mockHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error {
-	helm.repo = []string{name, repository, cafile, certfile, keyfile, username, password}
-	return nil
-}
-func (helm *mockHelmExec) UpdateRepo() error {
-	return nil
-}
-func (helm *mockHelmExec) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
-	if strings.Contains(name, "error") {
-		return errors.New("error")
-	}
-	helm.releases = append(helm.releases, mockRelease{name: name, flags: flags})
-	helm.charts = append(helm.charts, chart)
-	return nil
-}
-func (helm *mockHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
-	helm.diffed = append(helm.diffed, mockRelease{name: name, flags: flags})
-	return nil
-}
-func (helm *mockHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
-	if strings.Contains(release, "error") {
-		return errors.New("error")
-	}
-	helm.releases = append(helm.releases, mockRelease{name: release, flags: flags})
-	return nil
-}
-func (helm *mockHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
-	if strings.Contains(name, "error") {
-		return errors.New("error")
-	}
-	helm.deleted = append(helm.deleted, mockRelease{name: name, flags: flags})
-	return nil
-}
-func (helm *mockHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
-	return helm.lists[listKey{filter: filter, flags: strings.Join(flags, "")}], nil
-}
-func (helm *mockHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
-	return "", nil
-}
-func (helm *mockHelmExec) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
-	if strings.Contains(name, "error") {
-		return errors.New("error")
-	}
-	helm.releases = append(helm.releases, mockRelease{name: name, flags: flags})
-	return nil
-}
-func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
-	return nil
-}
-func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error {
-	return nil
-}
-func (helm *mockHelmExec) TemplateRelease(name, chart string, flags ...string) error {
-	return nil
-}
 func TestHelmState_SyncRepos(t *testing.T) {
 	tests := []struct {
 		name  string
 		repos []RepositorySpec
-		helm  *mockHelmExec
+		helm  *exectest.Helm
 		envs  map[string]string
 		want  []string
 	}{
@@ -798,7 +682,7 @@ func TestHelmState_SyncRepos(t *testing.T) {
 					Password: "",
 				},
 			},
-			helm: &mockHelmExec{},
+			helm: &exectest.Helm{},
 			want: []string{"name", "http://example.com/", "", "", "", "", ""},
 		},
 		{
@@ -813,7 +697,7 @@ func TestHelmState_SyncRepos(t *testing.T) {
 					Password: "",
 				},
 			},
-			helm: &mockHelmExec{},
+			helm: &exectest.Helm{},
 			want: []string{"name", "http://example.com/", "", "certfile", "keyfile", "", ""},
 		},
 		{
@@ -827,7 +711,7 @@ func TestHelmState_SyncRepos(t *testing.T) {
 					Password: "",
 				},
 			},
-			helm: &mockHelmExec{},
+			helm: &exectest.Helm{},
 			want: []string{"name", "http://example.com/", "cafile", "", "", "", ""},
 		},
 		{
@@ -842,7 +726,7 @@ func TestHelmState_SyncRepos(t *testing.T) {
 					Password: "example_password",
 				},
 			},
-			helm: &mockHelmExec{},
+			helm: &exectest.Helm{},
 			want: []string{"name", "http://example.com/", "", "", "", "example_user", "example_password"},
 		},
 	}
@@ -858,8 +742,8 @@ func TestHelmState_SyncRepos(t *testing.T) {
 			state := &HelmState{
 				Repositories: tt.repos,
 			}
-			if _ = state.SyncRepos(tt.helm); !reflect.DeepEqual(tt.helm.repo, tt.want) {
-				t.Errorf("HelmState.SyncRepos() for [%s] = %v, want %v", tt.name, tt.helm.repo, tt.want)
+			if _ = state.SyncRepos(tt.helm); !reflect.DeepEqual(tt.helm.Repo, tt.want) {
+				t.Errorf("HelmState.SyncRepos() for [%s] = %v, want %v", tt.name, tt.helm.Repo, tt.want)
 			}
 		})
 	}
@@ -869,8 +753,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 	tests := []struct {
 		name          string
 		releases      []ReleaseSpec
-		helm          *mockHelmExec
-		wantReleases  []mockRelease
+		helm          *exectest.Helm
+		wantReleases  []exectest.Release
 		wantErrorMsgs []string
 	}{
 		{
@@ -881,8 +765,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 					Chart: "foo",
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{}}},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{}}},
 		},
 		{
 			name: "with tiller args",
@@ -893,8 +777,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 					TillerNamespace: "tillerns",
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--tiller-namespace", "tillerns"}}},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--tiller-namespace", "tillerns"}}},
 		},
 		{
 			name: "escaped values",
@@ -914,8 +798,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 					},
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}}},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}}},
 		},
 		{
 			name: "set single value from file",
@@ -939,8 +823,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 					},
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}}},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}}},
 		},
 		{
 			name: "set single array value in an array",
@@ -959,108 +843,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 					},
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
-		},
-		{
-			name: "foo needs bar",
-			releases: []ReleaseSpec{
-				{
-					Name:  "foo",
-					Chart: "charts/foo",
-					Needs: []string{
-						"bar",
-					},
-				},
-				{
-					Name:  "bar",
-					Chart: "charts/bar",
-				},
-			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"bar", []string{}}, {"foo", []string{}}},
-		},
-		{
-			name: "bar needs foo",
-			releases: []ReleaseSpec{
-				{
-					Name:  "foo",
-					Chart: "charts/foo",
-				},
-				{
-					Name:  "bar",
-					Chart: "charts/bar",
-					Needs: []string{
-						"foo",
-					},
-				},
-			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"foo", []string{}}, {"bar", []string{}}},
-		},
-		{
-			name: "ns2/bar needs ns1/foo",
-			releases: []ReleaseSpec{
-				{
-					Name:      "foo",
-					Namespace: "ns1",
-					Chart:     "charts/foo",
-				},
-				{
-					Name:      "bar",
-					Namespace: "ns2",
-					Chart:     "charts/bar",
-					Needs: []string{
-						"ns1/foo",
-					},
-				},
-			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"foo", []string{"--namespace", "ns1"}}, {"bar", []string{"--namespace", "ns2"}}},
-		},
-		{
-			name: "tillerns1/ns1/foo needs tillerns2/ns2/bar",
-			releases: []ReleaseSpec{
-				{
-					Name:            "foo",
-					Chart:           "charts/foo",
-					Namespace:       "ns1",
-					TillerNamespace: "tillerns1",
-					Needs: []string{
-						"tillerns2/ns2/bar",
-					},
-				},
-				{
-					Name:            "bar",
-					Namespace:       "ns2",
-					TillerNamespace: "tillerns2",
-					Chart:           "charts/bar",
-				},
-			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"bar", []string{"--tiller-namespace", "tillerns2", "--namespace", "ns2"}}, {"foo", []string{"--tiller-namespace", "tillerns1", "--namespace", "ns1"}}},
-		},
-		{
-			name: "tillerns1/ns1/foo needs tillerns2/ns2/bar",
-			releases: []ReleaseSpec{
-				{
-					Name:            "foo",
-					Chart:           "charts/foo",
-					Namespace:       "ns1",
-					TillerNamespace: "tillerns1",
-					Needs: []string{
-						"bar",
-					},
-				},
-				{
-					Name:            "bar",
-					Namespace:       "ns2",
-					TillerNamespace: "tillerns2",
-					Chart:           "charts/bar",
-				},
-			},
-			helm:          &mockHelmExec{},
-			wantErrorMsgs: []string{`"tillerns1/ns1/foo" needs "bar", but it must be one of tillerns1/ns1/foo, tillerns2/ns2/bar`},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}"}}},
 		},
 	}
 	for i := range tests {
@@ -1087,8 +871,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
 					t.Fatalf("%d unexpected errors detected", mismatch)
 				}
 			}
-			if !reflect.DeepEqual(tt.helm.releases, tt.wantReleases) {
-				t.Errorf("HelmState.SyncReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.wantReleases)
+			if !reflect.DeepEqual(tt.helm.Releases, tt.wantReleases) {
+				t.Errorf("HelmState.SyncReleases() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.wantReleases)
 			}
 		})
 	}
@@ -1177,11 +961,11 @@ func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing.
 			}
 			fs := testhelper.NewTestFs(map[string]string{})
 			state = injectFs(state, fs)
-			helm := &mockHelmExec{
-				lists: map[listKey]string{},
+			helm := &exectest.Helm{
+				Lists: map[exectest.ListKey]string{},
 			}
 			//simulate the helm.list call result
-			helm.lists[listKey{filter: "^" + tt.release.Name + "$"}] = tt.listResult
+			helm.Lists[exectest.ListKey{Filter: "^" + tt.release.Name + "$"}] = tt.listResult
 
 			affectedReleases := AffectedReleases{}
 			errs := state.SyncReleases(&affectedReleases, helm, []string{}, 1)
@@ -1213,7 +997,7 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
 		name         string
 		releases     []ReleaseSpec
 		installed    []bool
-		wantAffected mockAffected
+		wantAffected exectest.Affected
 	}{
 		{
 			name: "2 release",
@@ -1227,7 +1011,14 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
 					Chart: "bar",
 				},
 			},
-			wantAffected: mockAffected{[]*mockRelease{{"releaseNameFoo", []string{}}, {"releaseNameBar", []string{}}}, nil, nil},
+			wantAffected: exectest.Affected{
+				Upgraded: []*exectest.Release{
+					{Name: "releaseNameFoo", Flags: []string{}},
+					{Name: "releaseNameBar", Flags: []string{}},
+				},
+				Deleted: nil,
+				Failed:  nil,
+			},
 		},
 		{
 			name: "2 removed",
@@ -1243,8 +1034,15 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
 					Installed: &no,
 				},
 			},
-			installed:    []bool{true, true},
-			wantAffected: mockAffected{nil, []*mockRelease{{"releaseNameFoo", []string{}}, {"releaseNameBar", []string{}}}, nil},
+			installed: []bool{true, true},
+			wantAffected: exectest.Affected{
+				Upgraded: nil,
+				Deleted: []*exectest.Release{
+					{Name: "releaseNameFoo", Flags: []string{}},
+					{Name: "releaseNameBar", Flags: []string{}},
+				},
+				Failed: nil,
+			},
 		},
 		{
 			name: "2 errors",
@@ -1258,7 +1056,14 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
 					Chart: "foo",
 				},
 			},
-			wantAffected: mockAffected{nil, nil, []*mockRelease{{"releaseNameFoo-error", []string{}}, {"releaseNameBar-error", []string{}}}},
+			wantAffected: exectest.Affected{
+				Upgraded: nil,
+				Deleted:  nil,
+				Failed: []*exectest.Release{
+					{Name: "releaseNameFoo-error", Flags: []string{}},
+					{Name: "releaseNameBar-error", Flags: []string{}},
+				},
+			},
 		},
 		{
 			name: "1 removed, 1 new, 1 error",
@@ -1277,8 +1082,18 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
 					Chart: "foo",
 				},
 			},
-			installed:    []bool{true, true, true},
-			wantAffected: mockAffected{[]*mockRelease{{"releaseNameFoo", []string{}}}, []*mockRelease{{"releaseNameBar", []string{}}}, []*mockRelease{{"releaseNameFoo-error", []string{}}}},
+			installed: []bool{true, true, true},
+			wantAffected: exectest.Affected{
+				Upgraded: []*exectest.Release{
+					{Name: "releaseNameFoo", Flags: []string{}},
+				},
+				Deleted: []*exectest.Release{
+					{Name: "releaseNameBar", Flags: []string{}},
+				},
+				Failed: []*exectest.Release{
+					{Name: "releaseNameFoo-error", Flags: []string{}},
+				},
+			},
 		},
 	}
 	for i := range tests {
@@ -1289,33 +1104,33 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
 				logger:      logger,
 				valsRuntime: valsRuntime,
 			}
-			helm := &mockHelmExec{
-				lists: map[listKey]string{},
+			helm := &exectest.Helm{
+				Lists: map[exectest.ListKey]string{},
 			}
 			//simulate the release is already installed
 			for i, release := range tt.releases {
 				if tt.installed != nil && tt.installed[i] {
-					helm.lists[listKey{filter: "^" + release.Name + "$"}] = release.Name
+					helm.Lists[exectest.ListKey{Filter: "^" + release.Name + "$"}] = release.Name
 				}
 			}
 
 			affectedReleases := AffectedReleases{}
 			if err := state.SyncReleases(&affectedReleases, helm, []string{}, 1); err != nil {
-				if !testEq(affectedReleases.Failed, tt.wantAffected.failed) {
-					t.Errorf("HelmState.SynchAffectedRelease() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.failed)
+				if !testEq(affectedReleases.Failed, tt.wantAffected.Failed) {
+					t.Errorf("HelmState.SynchAffectedRelease() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.Failed)
 				} //else expected error
 			}
-			if !testEq(affectedReleases.Upgraded, tt.wantAffected.upgraded) {
-				t.Errorf("HelmState.SynchAffectedRelease() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.upgraded)
+			if !testEq(affectedReleases.Upgraded, tt.wantAffected.Upgraded) {
+				t.Errorf("HelmState.SynchAffectedRelease() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.Upgraded)
 			}
-			if !testEq(affectedReleases.Deleted, tt.wantAffected.deleted) {
-				t.Errorf("HelmState.SynchAffectedRelease() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.deleted)
+			if !testEq(affectedReleases.Deleted, tt.wantAffected.Deleted) {
+				t.Errorf("HelmState.SynchAffectedRelease() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.Deleted)
 			}
 		})
 	}
 }
 
-func testEq(a []*ReleaseSpec, b []*mockRelease) bool {
+func testEq(a []*ReleaseSpec, b []*exectest.Release) bool {
 
 	// If one is nil, the other must also be nil.
 	if (a == nil) != (b == nil) {
@@ -1327,7 +1142,7 @@ func testEq(a []*ReleaseSpec, b []*mockRelease) bool {
 	}
 
 	for i := range a {
-		if a[i].Name != b[i].name {
+		if a[i].Name != b[i].Name {
 			return false
 		}
 	}
@@ -1391,11 +1206,11 @@ func TestGetDeployedVersion(t *testing.T) {
 				logger:      logger,
 				valsRuntime: valsRuntime,
 			}
-			helm := &mockHelmExec{
-				lists: map[listKey]string{},
+			helm := &exectest.Helm{
+				Lists: map[exectest.ListKey]string{},
 			}
 			//simulate the helm.list call result
-			helm.lists[listKey{filter: "^" + tt.release.Name + "$"}] = tt.listResult
+			helm.Lists[exectest.ListKey{Filter: "^" + tt.release.Name + "$"}] = tt.listResult
 
 			affectedReleases := AffectedReleases{}
 			state.SyncReleases(&affectedReleases, helm, []string{}, 1)
@@ -1411,8 +1226,8 @@ func TestHelmState_DiffReleases(t *testing.T) {
 	tests := []struct {
 		name         string
 		releases     []ReleaseSpec
-		helm         *mockHelmExec
-		wantReleases []mockRelease
+		helm         *exectest.Helm
+		wantReleases []exectest.Release
 	}{
 		{
 			name: "normal release",
@@ -1422,8 +1237,8 @@ func TestHelmState_DiffReleases(t *testing.T) {
 					Chart: "foo",
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{}}},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{}}},
 		},
 		{
 			name: "with tiller args",
@@ -1434,8 +1249,8 @@ func TestHelmState_DiffReleases(t *testing.T) {
 					TillerNamespace: "tillerns",
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--tiller-namespace", "tillerns"}}},
+			helm:         &exectest.Helm{},
+			wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--tiller-namespace", "tillerns"}}},
 		},
 		{
 			name: "escaped values",
@@ -1455,8 +1270,10 @@ func TestHelmState_DiffReleases(t *testing.T) {
 					},
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}}},
+			helm: &exectest.Helm{},
+			wantReleases: []exectest.Release{
+				{Name: "releaseName", Flags: []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}},
+			},
 		},
 		{
 			name: "set single value from file",
@@ -1480,8 +1297,10 @@ func TestHelmState_DiffReleases(t *testing.T) {
 					},
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}}},
+			helm: &exectest.Helm{},
+			wantReleases: []exectest.Release{
+				{Name: "releaseName", Flags: []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}},
+			},
 		},
 		{
 			name: "set single array value in an array",
@@ -1500,8 +1319,10 @@ func TestHelmState_DiffReleases(t *testing.T) {
 					},
 				},
 			},
-			helm:         &mockHelmExec{},
-			wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
+			helm: &exectest.Helm{},
+			wantReleases: []exectest.Release{
+				{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}"}},
+			},
 		},
 	}
 	for i := range tests {
@@ -1516,8 +1337,8 @@ func TestHelmState_DiffReleases(t *testing.T) {
 			if errs != nil && len(errs) > 0 {
 				t.Errorf("unexpected error: %v", errs)
 			}
-			if !reflect.DeepEqual(tt.helm.diffed, tt.wantReleases) {
-				t.Errorf("HelmState.DiffReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.wantReleases)
+			if !reflect.DeepEqual(tt.helm.Diffed, tt.wantReleases) {
+				t.Errorf("HelmState.DiffReleases() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.wantReleases)
 			}
 		})
 	}
@@ -1527,7 +1348,7 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
 	tests := []struct {
 		name                    string
 		releases                []ReleaseSpec
-		helm                    *mockHelmExec
+		helm                    *exectest.Helm
 		expectedNumRemovedFiles int
 	}{
 		{
@@ -1538,7 +1359,7 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
 					Chart: "foo",
 				},
 			},
-			helm:                    &mockHelmExec{},
+			helm:                    &exectest.Helm{},
 			expectedNumRemovedFiles: 0,
 		},
 		{
@@ -1554,7 +1375,7 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
 					},
 				},
 			},
-			helm:                    &mockHelmExec{},
+			helm:                    &exectest.Helm{},
 			expectedNumRemovedFiles: 1,
 		},
 		{
@@ -1571,7 +1392,7 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
 					},
 				},
 			},
-			helm:                    &mockHelmExec{},
+			helm:                    &exectest.Helm{},
 			expectedNumRemovedFiles: 2,
 		},
 	}
@@ -1611,7 +1432,7 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
 	tests := []struct {
 		name                    string
 		releases                []ReleaseSpec
-		helm                    *mockHelmExec
+		helm                    *exectest.Helm
 		expectedNumRemovedFiles int
 	}{
 		{
@@ -1622,7 +1443,7 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
 					Chart: "foo",
 				},
 			},
-			helm:                    &mockHelmExec{},
+			helm:                    &exectest.Helm{},
 			expectedNumRemovedFiles: 0,
 		},
 		{
@@ -1638,7 +1459,7 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
 					},
 				},
 			},
-			helm:                    &mockHelmExec{},
+			helm:                    &exectest.Helm{},
 			expectedNumRemovedFiles: 1,
 		},
 		{
@@ -1655,7 +1476,7 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
 					},
 				},
 			},
-			helm:                    &mockHelmExec{},
+			helm:                    &exectest.Helm{},
 			expectedNumRemovedFiles: 2,
 		},
 	}
@@ -1693,8 +1514,8 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
 }
 
 func TestHelmState_UpdateDeps(t *testing.T) {
-	helm := &mockHelmExec{
-		updateDepsCallbacks: map[string]func(string) error{},
+	helm := &exectest.Helm{
+		UpdateDepsCallbacks: map[string]func(string) error{},
 	}
 
 	var generatedDir string
@@ -1704,7 +1525,7 @@ func TestHelmState_UpdateDeps(t *testing.T) {
 		if err != nil {
 			return "", err
 		}
-		helm.updateDepsCallbacks[generatedDir] = func(chart string) error {
+		helm.UpdateDepsCallbacks[generatedDir] = func(chart string) error {
 			content := []byte(`dependencies:
 - name: envoy
   repository: https://kubernetes-charts.storage.googleapis.com
@@ -1763,8 +1584,8 @@ generated: 2019-05-16T15:42:45.50486+09:00
 
 	errs := state.UpdateDeps(helm)
 	want := []string{"/", "/examples", "/helmfile", "/src/published", generatedDir}
-	if !reflect.DeepEqual(helm.charts, want) {
-		t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.charts, want)
+	if !reflect.DeepEqual(helm.Charts, want) {
+		t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.Charts, want)
 	}
 	if len(errs) != 0 {
 		t.Errorf("HelmState.UpdateDeps() - no errors, but got %d: %v", len(errs), errs)
@@ -1833,8 +1654,8 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 	tests := []struct {
 		name     string
 		releases []ReleaseSpec
-		helm     *mockHelmExec
-		want     []mockRelease
+		helm     *exectest.Helm
+		want     []exectest.Release
 		wantErr  bool
 	}{
 		{
@@ -1844,8 +1665,10 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 					Name: "releaseA",
 				},
 			},
-			helm: &mockHelmExec{},
-			want: []mockRelease{{"releaseA", []string{}}},
+			helm: &exectest.Helm{},
+			want: []exectest.Release{
+				{Name: "releaseA", Flags: []string{}},
+			},
 		},
 		{
 			name: "happy path",
@@ -1854,7 +1677,7 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 					Name: "error",
 				},
 			},
-			helm:    &mockHelmExec{},
+			helm:    &exectest.Helm{},
 			wantErr: true,
 		},
 		{
@@ -1867,7 +1690,7 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 					},
 				},
 			},
-			helm:    &mockHelmExec{},
+			helm:    &exectest.Helm{},
 			wantErr: true,
 		},
 		{
@@ -1881,7 +1704,7 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 					Installed: boolValue(false),
 				},
 			},
-			helm:    &mockHelmExec{},
+			helm:    &exectest.Helm{},
 			wantErr: false,
 		},
 		{
@@ -1892,8 +1715,10 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 					TillerNamespace: "tillerns",
 				},
 			},
-			helm: &mockHelmExec{},
-			want: []mockRelease{{"releaseA", []string{"--tiller-namespace", "tillerns"}}},
+			helm: &exectest.Helm{},
+			want: []exectest.Release{
+				{Name: "releaseA", Flags: []string{"--tiller-namespace", "tillerns"}},
+			},
 		},
 	}
 	for i := range tests {
@@ -1920,8 +1745,8 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
 				t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
 				return
 			}
-			if !reflect.DeepEqual(tt.helm.releases, tt.want) {
-				t.Errorf("HelmState.ReleaseStatuses() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.want)
+			if !reflect.DeepEqual(tt.helm.Releases, tt.want) {
+				t.Errorf("HelmState.ReleaseStatuses() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.want)
 			}
 		}
 		t.Run(tt.name, f)
@@ -1933,8 +1758,8 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
 		name            string
 		cleanup         bool
 		releases        []ReleaseSpec
-		helm            *mockHelmExec
-		want            []mockRelease
+		helm            *exectest.Helm
+		want            []exectest.Release
 		wantErr         bool
 		tillerNamespace string
 	}{
@@ -1945,8 +1770,8 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
 					Name: "releaseA",
 				},
 			},
-			helm: &mockHelmExec{},
-			want: []mockRelease{{"releaseA", []string{"--timeout", "1"}}},
+			helm: &exectest.Helm{},
+			want: []exectest.Release{{Name: "releaseA", Flags: []string{"--timeout", "1"}}},
 		},
 		{
 			name:    "do cleanup",
@@ -1956,8 +1781,8 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
 					Name: "releaseB",
 				},
 			},
-			helm: &mockHelmExec{},
-			want: []mockRelease{{"releaseB", []string{"--cleanup", "--timeout", "1"}}},
+			helm: &exectest.Helm{},
+			want: []exectest.Release{{Name: "releaseB", Flags: []string{"--cleanup", "--timeout", "1"}}},
 		},
 		{
 			name: "happy path",
@@ -1966,7 +1791,7 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
 					Name: "error",
 				},
 			},
-			helm:    &mockHelmExec{},
+			helm:    &exectest.Helm{},
 			wantErr: true,
 		},
 		{
@@ -1977,8 +1802,8 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
 					TillerNamespace: "tillerns",
 				},
 			},
-			helm: &mockHelmExec{},
-			want: []mockRelease{{"releaseA", []string{"--timeout", "1", "--tiller-namespace", "tillerns"}}},
+			helm: &exectest.Helm{},
+			want: []exectest.Release{{Name: "releaseA", Flags: []string{"--timeout", "1", "--tiller-namespace", "tillerns"}}},
 		},
 	}
 	for i := range tests {
@@ -1993,8 +1818,8 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
 				t.Errorf("TestReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
 				return
 			}
-			if !reflect.DeepEqual(tt.helm.releases, tt.want) {
-				t.Errorf("HelmState.TestReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.want)
+			if !reflect.DeepEqual(tt.helm.Releases, tt.want) {
+				t.Errorf("HelmState.TestReleases() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.want)
 			}
 		}
 		t.Run(tt.name, f)
@@ -2053,7 +1878,7 @@ func TestHelmState_NoReleaseMatched(t *testing.T) {
 func TestHelmState_Delete(t *testing.T) {
 	tests := []struct {
 		name            string
-		deleted         []mockRelease
+		deleted         []exectest.Release
 		wantErr         bool
 		desired         *bool
 		installed       bool
@@ -2069,7 +1894,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(true),
 			installed: true,
 			purge:     false,
-			deleted:   []mockRelease{{"releaseA", []string{}}},
+			deleted:   []exectest.Release{{Name: "releaseA", Flags: []string{}}},
 		},
 		{
 			name:      "desired(default) and installed (purge=false)",
@@ -2077,7 +1902,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   nil,
 			installed: true,
 			purge:     false,
-			deleted:   []mockRelease{{"releaseA", []string{}}},
+			deleted:   []exectest.Release{{Name: "releaseA", Flags: []string{}}},
 		},
 		{
 			name:      "desired(default) and installed (purge=false) but error",
@@ -2085,7 +1910,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   nil,
 			installed: true,
 			purge:     false,
-			deleted:   []mockRelease{{"releaseA", []string{}}},
+			deleted:   []exectest.Release{{Name: "releaseA", Flags: []string{}}},
 		},
 		{
 			name:      "desired and installed (purge=true)",
@@ -2093,7 +1918,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(true),
 			installed: true,
 			purge:     true,
-			deleted:   []mockRelease{{"releaseA", []string{"--purge"}}},
+			deleted:   []exectest.Release{{Name: "releaseA", Flags: []string{"--purge"}}},
 		},
 		{
 			name:      "desired but not installed (purge=false)",
@@ -2101,7 +1926,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(true),
 			installed: false,
 			purge:     false,
-			deleted:   []mockRelease{},
+			deleted:   []exectest.Release{},
 		},
 		{
 			name:      "desired but not installed (purge=true)",
@@ -2109,7 +1934,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(true),
 			installed: false,
 			purge:     true,
-			deleted:   []mockRelease{},
+			deleted:   []exectest.Release{},
 		},
 		{
 			name:      "installed but filtered (purge=false)",
@@ -2117,7 +1942,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(false),
 			installed: true,
 			purge:     false,
-			deleted:   []mockRelease{},
+			deleted:   []exectest.Release{},
 		},
 		{
 			name:      "installed but filtered (purge=true)",
@@ -2125,7 +1950,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(false),
 			installed: true,
 			purge:     true,
-			deleted:   []mockRelease{},
+			deleted:   []exectest.Release{},
 		},
 		{
 			name:      "not installed, and filtered (purge=false)",
@@ -2133,7 +1958,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(false),
 			installed: false,
 			purge:     false,
-			deleted:   []mockRelease{},
+			deleted:   []exectest.Release{},
 		},
 		{
 			name:      "not installed, and filtered (purge=true)",
@@ -2141,7 +1966,7 @@ func TestHelmState_Delete(t *testing.T) {
 			desired:   boolValue(false),
 			installed: false,
 			purge:     true,
-			deleted:   []mockRelease{},
+			deleted:   []exectest.Release{},
 		},
 		{
 			name:            "with tiller args",
@@ -2151,7 +1976,7 @@ func TestHelmState_Delete(t *testing.T) {
 			purge:           true,
 			tillerNamespace: "tillerns",
 			flags:           "--tiller-namespacetillerns",
-			deleted:         []mockRelease{{"releaseA", []string{"--purge", "--tiller-namespace", "tillerns"}}},
+			deleted:         []exectest.Release{{Name: "releaseA", Flags: []string{"--purge", "--tiller-namespace", "tillerns"}}},
 		},
 		{
 			name:        "with kubecontext",
@@ -2161,7 +1986,7 @@ func TestHelmState_Delete(t *testing.T) {
 			purge:       true,
 			kubeContext: "ctx",
 			flags:       "--kube-contextctx",
-			deleted:     []mockRelease{{"releaseA", []string{"--purge", "--kube-context", "ctx"}}},
+			deleted:     []exectest.Release{{Name: "releaseA", Flags: []string{"--purge", "--kube-context", "ctx"}}},
 		},
 		{
 			name:           "with default kubecontext",
@@ -2171,7 +1996,7 @@ func TestHelmState_Delete(t *testing.T) {
 			purge:          true,
 			defKubeContext: "defctx",
 			flags:          "--kube-contextdefctx",
-			deleted:        []mockRelease{{"releaseA", []string{"--purge", "--kube-context", "defctx"}}},
+			deleted:        []exectest.Release{{Name: "releaseA", Flags: []string{"--purge", "--kube-context", "defctx"}}},
 		},
 		{
 			name:           "with non-default and default kubecontexts",
@@ -2182,7 +2007,7 @@ func TestHelmState_Delete(t *testing.T) {
 			kubeContext:    "ctx",
 			defKubeContext: "defctx",
 			flags:          "--kube-contextctx",
-			deleted:        []mockRelease{{"releaseA", []string{"--purge", "--kube-context", "ctx"}}},
+			deleted:        []exectest.Release{{Name: "releaseA", Flags: []string{"--purge", "--kube-context", "ctx"}}},
 		},
 	}
 	for i := range tests {
@@ -2208,12 +2033,12 @@ func TestHelmState_Delete(t *testing.T) {
 				Releases: releases,
 				logger:   logger,
 			}
-			helm := &mockHelmExec{
-				lists:   map[listKey]string{},
-				deleted: []mockRelease{},
+			helm := &exectest.Helm{
+				Lists:   map[exectest.ListKey]string{},
+				Deleted: []exectest.Release{},
 			}
 			if tt.installed {
-				helm.lists[listKey{filter: "^" + name + "$", flags: tt.flags}] = name
+				helm.Lists[exectest.ListKey{Filter: "^" + name + "$", Flags: tt.flags}] = name
 			}
 			affectedReleases := AffectedReleases{}
 			errs := state.DeleteReleases(&affectedReleases, helm, 1, tt.purge)
@@ -2222,8 +2047,8 @@ func TestHelmState_Delete(t *testing.T) {
 					t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
 					return
 				}
-			} else if !(reflect.DeepEqual(tt.deleted, helm.deleted) && (len(affectedReleases.Deleted) == len(tt.deleted))) {
-				t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.deleted)
+			} else if !(reflect.DeepEqual(tt.deleted, helm.Deleted) && (len(affectedReleases.Deleted) == len(tt.deleted))) {
+				t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.Deleted)
 			}
 		}
 		t.Run(tt.name, f)
diff --git a/pkg/testhelper/diff.go b/pkg/testhelper/diff.go
new file mode 100644
index 0000000..3285daa
--- /dev/null
+++ b/pkg/testhelper/diff.go
@@ -0,0 +1,103 @@
+package testhelper
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"math"
+	"strings"
+
+	"github.com/aryann/difflib"
+)
+
+func Diff(want, got string, context int) (string, bool) {
+	records := difflib.Diff(
+		strings.Split(want, "\n"),
+		strings.Split(got, "\n"),
+	)
+
+	w := &bytes.Buffer{}
+
+	changed := checkAndPrintRecords(w, records, context)
+
+	return w.String(), changed
+}
+
+func checkAndPrintRecords(w io.Writer, records []difflib.DiffRecord, context int) bool {
+	var changed bool
+	if context >= 0 {
+		distances := calculateDistances(records)
+		omitting := false
+		for i, diff := range records {
+			if diff.Delta != difflib.Common {
+				changed = true
+			}
+			if distances[i] > context {
+				if !omitting {
+					fmt.Fprintln(w, "...")
+					omitting = true
+				}
+			} else {
+				omitting = false
+				fmt.Fprintln(w, formatRecord(diff))
+			}
+		}
+	} else {
+		for _, diff := range records {
+			if diff.Delta != difflib.Common {
+				changed = true
+			}
+			fmt.Fprintln(w, formatRecord(diff))
+		}
+	}
+	return changed
+}
+
+func formatRecord(diff difflib.DiffRecord) string {
+	var prefix string
+	switch diff.Delta {
+	case difflib.RightOnly:
+		prefix = "+ "
+	case difflib.LeftOnly:
+		prefix = "- "
+	case difflib.Common:
+		prefix = "  "
+	}
+
+	return prefix + diff.Payload
+}
+
+// Shamelessly and thankfully copied from https://github.com/databus23/helm-diff/blob/99b8474af7726ca6f57b37b0b8b8f3cd36c991e8/diff/diff.go#L116
+func calculateDistances(diffs []difflib.DiffRecord) map[int]int {
+	distances := map[int]int{}
+
+	// Iterate forwards through diffs, set 'distance' based on closest 'change' before this line
+	change := -1
+	for i, diff := range diffs {
+		if diff.Delta != difflib.Common {
+			change = i
+		}
+		distance := math.MaxInt32
+		if change != -1 {
+			distance = i - change
+		}
+		distances[i] = distance
+	}
+
+	// Iterate backwards through diffs, reduce 'distance' based on closest 'change' after this line
+	change = -1
+	for i := len(diffs) - 1; i >= 0; i-- {
+		diff := diffs[i]
+		if diff.Delta != difflib.Common {
+			change = i
+		}
+		if change != -1 {
+			distance := change - i
+			if distance < distances[i] {
+				distances[i] = distance
+			}
+		}
+	}
+
+	return distances
+}