summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjannfis <jann@mistrust.net>2020-08-04 19:45:46 +0200
committerjannfis <jann@mistrust.net>2020-08-04 19:45:46 +0200
commitbb184543e516f17c5801242645b5d77d0244c538 (patch)
tree79913d38a3f4566a4547d0923452a625518437c3
Initial commit
-rw-r--r--.dockerignore9
-rw-r--r--.gitignore15
-rw-r--r--CHANGELOG.md17
-rw-r--r--Dockerfile17
-rw-r--r--LICENSE201
-rw-r--r--Makefile60
-rw-r--r--OWNERS6
-rw-r--r--README.md98
-rw-r--r--cmd/main.go374
-rw-r--r--config/example-config.yaml37
-rw-r--r--docs/README.md3
-rw-r--r--docs/assets/extra.css10
-rw-r--r--docs/assets/logo.pngbin0 -> 27971 bytes
-rw-r--r--docs/configuration/applications.md47
-rw-r--r--docs/configuration/basics.md0
-rw-r--r--docs/configuration/images.md120
-rw-r--r--docs/configuration/registries.md94
-rw-r--r--docs/index.md67
-rw-r--r--docs/install/start.md158
-rw-r--r--go.mod56
-rw-r--r--go.sum946
-rwxr-xr-xhack/generate-manifests.sh30
-rw-r--r--manifests/base/config/argocd-image-updater-cm.yaml7
-rw-r--r--manifests/base/config/argocd-image-updater-secret.yaml7
-rw-r--r--manifests/base/config/kustomization.yaml6
-rw-r--r--manifests/base/deployment/argocd-image-updater-deployment.yaml55
-rw-r--r--manifests/base/deployment/kustomization.yaml5
-rw-r--r--manifests/base/kustomization.yaml13
-rw-r--r--manifests/base/rbac/argocd-image-updater-sa.yaml8
-rw-r--r--manifests/base/rbac/kustomization.yaml5
-rw-r--r--manifests/install.yaml80
-rw-r--r--mkdocs.yml28
-rw-r--r--pkg/argocd/argocd.go365
-rw-r--r--pkg/argocd/argocd_test.go146
-rw-r--r--pkg/client/kubernetes.go65
-rw-r--r--pkg/client/kubernetes_test.go72
-rw-r--r--pkg/common/constants.go14
-rw-r--r--pkg/config/config.go1
-rw-r--r--pkg/health/health.go24
-rw-r--r--pkg/image/credentials.go205
-rw-r--r--pkg/image/credentials_test.go198
-rw-r--r--pkg/image/image.go155
-rw-r--r--pkg/image/image_test.go88
-rw-r--r--pkg/image/kustomize.go39
-rw-r--r--pkg/image/version.go71
-rw-r--r--pkg/image/version_test.go59
-rw-r--r--pkg/log/log.go179
-rw-r--r--pkg/log/log_test.go155
-rw-r--r--pkg/registry/config.go64
-rw-r--r--pkg/registry/config_test.go19
-rw-r--r--pkg/registry/endpoints.go85
-rw-r--r--pkg/registry/endpoints_test.go98
-rw-r--r--pkg/registry/registry.go69
-rw-r--r--pkg/version/version.go28
-rw-r--r--test/README.md9
-rw-r--r--test/fake/kubernetes.go16
-rw-r--r--test/fixture/capture.go55
-rw-r--r--test/fixture/fileutil.go14
-rw-r--r--test/fixture/kubernetes.go33
-rw-r--r--test/testdata/docker/invalid1-config.json7
-rw-r--r--test/testdata/docker/valid-config.json7
-rw-r--r--test/testdata/kubernetes/config19
-rw-r--r--test/testdata/resources/dummy-secret.json12
63 files changed, 4950 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..c52389b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+.vscode/
+.idea/
+.DS_Store
+dist/
+*.iml
+# delve debug binaries
+cmd/**/debug
+debug.test
+coverage.out
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d4a7ee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.vscode/
+.idea/
+.DS_Store
+vendor/
+dist/
+site/
+*.iml
+# delve debug binaries
+cmd/**/debug
+debug.test
+coverage.out
+test-results
+.scannerwork
+.scratch
+*.goe
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..08aba6a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog for argocd-image-controller
+
+This is the change log for `argocd-image-controller`. Please read thoroughly
+when you upgrade versions, as there might be non-obvious changes that need
+handling on your side.
+
+## Release v0.1.0 - 2020-08-01
+
+Initial release.
+
+### Upgrade notes (no really, you MUST read this)
+
+### Bug fixes
+
+### New features
+
+### Other changes
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0e15bcc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,17 @@
+FROM golang:1.14.4 AS builder
+
+RUN mkdir -p /src/argocd-image-controller
+WORKDIR /src/argocd-image-controller
+COPY . .
+
+RUN mkdir -p dist && \
+ make controller
+
+FROM alpine:latest
+
+RUN mkdir -p /usr/local/bi n
+COPY --from=builder /src/argocd-image-controller/dist/argocd-image-controller /usr/local/bin/
+
+USER 1000
+
+ENTRYPOINT ["/usr/local/bin/argocd-image-controller"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9c8b506
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,60 @@
+LDFLAGS=-extldflags "-static"
+
+IMAGE_NAMESPACE?=argoproj-labs
+IMAGE_TAG?=latest
+IMAGE_NAME=argocd-image-updater
+ifdef IMAGE_NAMESPACE
+IMAGE_PREFIX=${IMAGE_NAMESPACE}/
+else
+IMAGE_PREFIX=
+endif
+
+all: prereq controller
+
+.PHONY: clean
+clean: clean-image
+ rm -rf vendor/
+
+.PHONY: clean-image
+clean-image:
+ rm -rf dist/
+ rm -f coverage.out
+
+mod-tidy:
+ go mod tidy
+
+mod-download:
+ go mod download
+
+mod-vendor:
+ go mod vendor
+
+.PHONY: test
+test:
+ go test -coverprofile coverage.out `go list ./... | egrep -v '(test|mocks)'`
+
+.PHONY: prereq
+prereq:
+ mkdir -p dist
+
+.PHONY: controller
+controller:
+ CGO_ENABLED=0 go build -o dist/argocd-image-controller cmd/main.go
+
+.PHONY: image
+image: clean-image mod-vendor
+ docker build -t ${IMAGE_PREFIX}${IMAGE_NAME}:${IMAGE_TAG} .
+ rm -rf vendor/
+
+.PHONY: manifests
+manifests:
+ ./hack/generate-manifests.sh
+
+.PHONY: run-test
+run-test:
+ docker run -v $(HOME)/.kube:/kube --rm -it \
+ -e ARGOCD_TOKEN \
+ argocd-image-controller \
+ --kubeconfig /kube/config \
+ --argocd-server-addr $(ARGOCD_SERVER) \
+ --grpc-web
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..af5da3f
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,6 @@
+owners:
+- jannfis
+
+approvers:
+
+reviewers: \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f0b5b7c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,98 @@
+# ArgoCD Image Updater
+
+## Introduction
+
+ArgoCD Image Updater is a tool to automatically update the container
+images of Kubernetes workloads which are managed by ArgoCD.
+
+Currently it will only work with applications that are built using *Kustomize*
+or *Helm* tooling. Applications built from plain YAML or custom tools are not
+supported yet (and maybe never will).
+
+## Documentation
+
+Read
+[the documentation](https://argocd-image-updater.readthedocs.io)
+for more information on how to setup and run ArgoCD Image Updater and to get
+known to it's features and limitations.
+
+## Current status
+
+**Disclaimer: This is pre-release code. It might have bugs that will
+break things in unexpected way.**
+
+ArgoCD Image Updater was born just recently, and is not suitable for
+production workloads yet. You are welcome to test it in your non-critical
+environments, and to contribute by filing bugs, enhancement requests or even
+better, sending in pull requests.
+
+We decided to publish the code early, so that the community can be involved
+early on in the development process, too.
+
+**Important note:** Until the first stable version (i.e. `v1.0`) is released,
+breaking changes between the releases must be expected. We will do our best
+to indicate all breaking changes (and how to un-break them) in the
+[Changelog](CHANGELOG.md)
+
+## Contributing
+
+You are welcome to contribute to this project by means of raising issues for
+bugs, sending & discussing enhancment ideas or by contributing code via pull
+requests.
+
+In any case, please be sure that you have read & understood the currently known
+design limitations before raising issues.
+
+Also, if you want to contribute code, please make sure that your code
+
+* has its functionality covered by unit tests (coverage goal is 80%),
+* is correctly linted,
+* is well commented,
+* and last but not least is compatible with our license and CLA
+
+## License
+
+`argocd-image-updater` is open source software, released under the
+[Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0)
+
+## Things that are planned (roadmap)
+
+The following things are on the roadmap until the `v1.0` release.
+
+* Extend ArgoCD functionality to be able to update images for other types of
+ applications.
+
+* Provide web hook support to trigger update check for a given image
+
+* Use concurrency for updating multiple applications at once
+
+* Improve error handling
+
+* Support for image tags with i.e. Git commit SHAs
+
+## Frequently asked questions
+
+**Does it write back the changes to Git?**
+
+No, and this feature is also not planned for any of the next releases. We think
+it's close to impossible to get such a feature 100% correctly working, because
+there are so many edge-cases to consider (i.e. possible merge conflicts) and
+there's no easy way to find out where a certain resource lives in Git when
+manifests are rendered through a tool.
+
+**How does it persist the changes then?**
+
+The ArgoCD Image Updater leverages the ArgoCD API to set application paramaters,
+and ArgoCD will then persist the change in the application's manifest. This is
+something ArgoCD will not overwrite upon the next manual (or automatic) sync,
+except when the overrides are explicitly set in the manifest.
+
+**Are there plans to extend functionality beyond Kustomize or Helm?**
+
+Not yet, since we are dependent upon what functionality ArgoCD provides for
+these types of applications.
+
+**Will it ever be fully integrated with ArgoCD?**
+
+In the current form, probably not. If there is community demand for it, let's
+see how we can make this happen.
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..8be72ad
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,374 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/argocd"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/client"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/health"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/registry"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/version"
+
+ "github.com/spf13/cobra"
+)
+
+var lastRun time.Time
+
+const DefaultArgoCDServerAddr = "argocd-server.argo-cd"
+
+// ImageUpdaterConfig contains global configuration and required runtime data
+type ImageUpdaterConfig struct {
+ ClientOpts argocd.ClientOptions
+ ArgocdNamespace string
+ DryRun bool
+ CheckInterval time.Duration
+ ArgoClient *argocd.ArgoCD
+ LogLevel string
+ KubeClient *client.KubernetesClient
+ MaxConcurrency int
+ HealthPort int
+ RegistriesConf string
+}
+
+// Stores some statistics about the results of a run
+type ImageUpdaterResult struct {
+ NumApplicationsProcessed int
+ NumImagesUpdated int
+ NumImagesConsidered int
+ NumSkipped int
+ NumErrors int
+}
+
+// Main loop for argocd-image-controller
+func runImageUpdater(cfg *ImageUpdaterConfig) (ImageUpdaterResult, error) {
+ result := ImageUpdaterResult{}
+ argoClient, err := argocd.NewClient(&cfg.ClientOpts)
+ if err != nil {
+ return result, err
+ }
+ cfg.ArgoClient = argoClient
+
+ apps, err := cfg.ArgoClient.ListApplications()
+ if err != nil {
+ log.WithContext().
+ AddField("argocd_server", cfg.ClientOpts.ServerAddr).
+ AddField("grpc_web", cfg.ClientOpts.GRPCWeb).
+ AddField("grpc_webroot", cfg.ClientOpts.GRPCWebRootPath).
+ AddField("plaintext", cfg.ClientOpts.Plaintext).
+ AddField("insecure", cfg.ClientOpts.Insecure).
+ Errorf("error while communicating with ArgoCD")
+ return result, err
+ }
+
+ // Get the list of applications that are allowed for updates, that is, those
+ // applications which have correct annotation.
+ appList, err := argocd.FilterApplicationsForUpdate(apps)
+ if err != nil {
+ return result, err
+ }
+
+ log.Debugf("Considering %d applications with annotations for update", len(appList))
+
+ for app, allowedImages := range appList {
+
+ // Get all images that are deployed with the current application
+ applicationImages, err := argoClient.GetImagesFromApplication(app)
+ if err != nil {
+ return result, err
+ }
+
+ result.NumApplicationsProcessed += 1
+
+ // Loop through all images of current application, and check whether one of
+ // its images is egilible for updating.
+ //
+ // Whether an image qualifies for update is dependent on semantic version
+ // constraints which are part of the application's annotation values.
+ //
+ for _, applicationImage := range applicationImages {
+ updateableImage := allowedImages.Images.ContainsImage(applicationImage, false)
+ if updateableImage == nil {
+ log.WithContext().AddField("application", app).Debugf("Image %s not in list of allowed images, skipping", applicationImage.ImageName)
+ result.NumSkipped += 1
+ continue
+ }
+
+ result.NumImagesConsidered += 1
+
+ imgCtx := log.WithContext().
+ AddField("application", app).
+ AddField("registry", applicationImage.RegistryURL).
+ AddField("image_name", applicationImage.ImageName).
+ AddField("image_tag", applicationImage.ImageTag)
+
+ imgCtx.Debugf("Considering this image for update")
+
+ rep, err := registry.GetRegistryEndpoint(applicationImage.RegistryURL)
+ if err != nil {
+ imgCtx.Errorf("Could not get registry endpoint from configuration: %v", err)
+ result.NumErrors += 1
+ continue
+ }
+
+ // Get list of available image tags from the repository
+ tags, err := rep.GetTags(applicationImage, cfg.KubeClient)
+ if err != nil {
+ imgCtx.Errorf("Could not get tags from registry: %v", err)
+ result.NumErrors += 1
+ continue
+ }
+
+ imgCtx.Tracef("List of available tags found: %v", tags)
+
+ // Get the latest available tag matching any constraint that might be set
+ // for allowed updates.
+ latest, err := applicationImage.GetNewestVersionFromTags(updateableImage.ImageTag, tags)
+ if err != nil {
+ imgCtx.Errorf("Unable to find newest version from available tags: %v", err)
+ result.NumErrors += 1
+ continue
+ }
+
+ // If we have no latest tag information, it means there was no tag which
+ // has met our version constraint (or there was no semantic versioned tag
+ // at all in the repository)
+ if latest == "" {
+ imgCtx.Debugf("No suitable image tag for upgrade found in list of available tags.")
+ result.NumSkipped += 1
+ continue
+ }
+
+ // If the latest tag does not match image's current tag, it means we have
+ // an update candidate.
+ if applicationImage.ImageTag != latest {
+ imgCtx.Infof("Upgrading image to %s", applicationImage.WithTag(latest).String())
+
+ if appType := argoClient.GetApplicationType(&allowedImages.Application); appType == argocd.ApplicationTypeKustomize {
+ err = argoClient.SetKustomizeImage(app, applicationImage.WithTag(latest))
+ } else if appType == argocd.ApplicationTypeHelm {
+ err = argoClient.SetHelmImage(app, applicationImage.WithTag(latest))
+ } else {
+ result.NumErrors += 1
+ err = fmt.Errorf("Could not update application %s - neither Helm nor Kustomize application", app)
+ }
+
+ if err != nil {
+ imgCtx.Errorf("Error while trying to update image: %v", err)
+ result.NumErrors += 1
+ continue
+ } else {
+ imgCtx.Infof("Successfully updated image '%s' to '%s'", applicationImage.GetFullNameWithTag(), applicationImage.WithTag(latest).GetFullNameWithTag())
+ result.NumImagesUpdated += 1
+ }
+ } else {
+ imgCtx.Debugf("Image '%s' already on latest allowed version", applicationImage.GetFullNameWithTag())
+ }
+ }
+ }
+
+ return result, nil
+}
+
+// Get boolean value from environment variable. Returns default value if env
+// is not set.
+func getBoolValFromEnv(env string, defaultValue bool) bool {
+ if val := os.Getenv(env); val != "" {
+ if strings.ToLower(val) == "true" {
+ return true
+ } else if strings.ToLower(val) == "false" {
+ return false
+ }
+ }
+ return defaultValue
+}
+
+func getServerAddrFromEnv() string {
+ if val := os.Getenv("ARGOCD_SERVER"); val != "" {
+ return val
+ }
+ return DefaultArgoCDServerAddr
+}
+
+func getPrintableInterval(interval time.Duration) string {
+ if interval == 0 {
+ return "once"
+ } else {
+ return interval.String()
+ }
+}
+
+func getPrintableHealthPort(port int) string {
+ if port == 0 {
+ return "off"
+ } else {
+ return fmt.Sprintf("%d", port)
+ }
+}
+
+func newCommand() error {
+ var cfg *ImageUpdaterConfig = &ImageUpdaterConfig{}
+ var once bool
+ var kubeConfig string
+ var disableKubernetes bool
+ var rootCmd = &cobra.Command{
+ Use: "argocd-image-updater",
+ Short: "Automatically update container images with ArgoCD",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := log.SetLogLevel(cfg.LogLevel); err != nil {
+ return err
+ }
+
+ if once {
+ cfg.CheckInterval = 0
+ cfg.HealthPort = 0
+ }
+
+ log.Infof("%s %s starting [loglevel:%s, interval:%s, healthport:%s]",
+ version.BinaryName(),
+ version.Version(),
+ strings.ToUpper(cfg.LogLevel),
+ getPrintableInterval(cfg.CheckInterval),
+ getPrintableHealthPort(cfg.HealthPort),
+ )
+
+ // Load registries configuration early on. We do not consider it a fatal
+ // error when the file does not exist, but we emit a warning.
+ if cfg.RegistriesConf != "" {
+ st, err := os.Stat(cfg.RegistriesConf)
+ if err != nil || st.IsDir() {
+ log.Warnf("Registry configuration at %s could not be read: %v -- using a default configuration", cfg.RegistriesConf, err)
+ } else {
+ err = registry.LoadRegistryConfiguration(cfg.RegistriesConf)
+ if err != nil {
+ log.Errorf("Could not load registry configuration from %s: %v", cfg.RegistriesConf, err)
+ return nil
+ }
+ }
+ }
+
+ if cfg.CheckInterval > 0 && cfg.CheckInterval < 60*time.Second {
+ log.Warnf("check interval is very low - it is not recommended to run below 1m0s")
+ }
+
+ var fullKubeConfigPath string
+ var err error
+
+ if !disableKubernetes {
+ if kubeConfig != "" {
+ fullKubeConfigPath, err = filepath.Abs(kubeConfig)
+ if err != nil {
+ log.Fatalf("Cannot expand path %s: %v", kubeConfig, err)
+ }
+ }
+
+ if fullKubeConfigPath != "" {
+ log.Debugf("Creating Kubernetes client from %s", fullKubeConfigPath)
+ } else {
+ log.Debugf("Creating in-cluster Kubernetes client")
+ }
+
+ cfg.KubeClient, err = client.NewKubernetesClient(fullKubeConfigPath)
+ if err != nil {
+ log.Fatalf("Cannot create kubernetes client: %v", err)
+ }
+ } else if kubeConfig != "" {
+ return fmt.Errorf("--kubeconfig and --disable-kubernetes cannot be specified together")
+ }
+
+ if token := os.Getenv("ARGOCD_TOKEN"); token != "" && cfg.ClientOpts.AuthToken == "" {
+ log.Debugf("Using ArgoCD API credentials from environment ARGOCD_TOKEN")
+ cfg.ClientOpts.AuthToken = token
+ }
+
+ log.Infof("ArgoCD configuration: [server=%s, auth_token=%v, insecure=%v, grpc_web=%v, plaintext=%v]",
+ cfg.ClientOpts.ServerAddr,
+ cfg.ClientOpts.AuthToken != "",
+ cfg.ClientOpts.Insecure,
+ cfg.ClientOpts.GRPCWeb,
+ cfg.ClientOpts.Plaintext,
+ )
+
+ // Health server will start in a go routine and run asynchronously
+ var hsErrCh chan error
+ if cfg.HealthPort > 0 {
+ log.Infof("Starting health probe server TCP port=%d", cfg.HealthPort)
+ hsErrCh = health.StartHealthServer(cfg.HealthPort)
+ }
+
+ // This is our main loop. We leave it only when our health probe server
+ // returns an error.
+ for {
+ select {
+ case err := <-hsErrCh:
+ if err != nil {
+ log.Errorf("Health probe server exited with error: %v", err)
+ return nil
+ } else {
+ log.Infof("Health probe server exited gracefully")
+ }
+ default:
+ if lastRun.IsZero() || time.Since(lastRun) > cfg.CheckInterval {
+ log.Debugf("Starting image update process")
+ result, err := runImageUpdater(cfg)
+ if err != nil {
+ log.Errorf("Error: %v", err)
+ } else if result.NumImagesUpdated > 0 || result.NumErrors > 0 {
+ log.Infof("Processing results: applications=%d images_considered=%d images_updated=%d errors=%d",
+ result.NumApplicationsProcessed,
+ result.NumImagesConsidered,
+ result.NumImagesUpdated,
+ result.NumErrors)
+ } else {
+ log.Debugf("Processing results: applications=%d images_considered=%d images_skipped=%d images_updated=%d errors=%d",
+ result.NumApplicationsProcessed,
+ result.NumImagesConsidered,
+ result.NumSkipped,
+ result.NumImagesUpdated,
+ result.NumErrors)
+ }
+ lastRun = time.Now()
+ }
+ }
+ if cfg.CheckInterval == 0 {
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+ log.Infof("Finished.")
+ return nil
+ },
+ }
+
+ rootCmd.Flags().StringVar(&cfg.ClientOpts.ServerAddr, "argocd-server-addr", getServerAddrFromEnv(), "address of ArgoCD API server")
+ rootCmd.Flags().BoolVar(&cfg.ClientOpts.GRPCWeb, "argocd-grpc-web", getBoolValFromEnv("ARGOCD_GRPC_WEB", false), "use grpc-web for connection to ArgoCD")
+ rootCmd.Flags().BoolVar(&cfg.ClientOpts.Insecure, "argocd-insecure", getBoolValFromEnv("ARGOCD_INSECURE", false), "(INSECURE) ignore invalid TLS certs for ArgoCD server")
+ rootCmd.Flags().BoolVar(&cfg.ClientOpts.Plaintext, "argocd-plaintext", getBoolValFromEnv("ARGOCD_PLAINTEXT", false), "(INSECURE) connect without TLS to ArgoCD server")
+ rootCmd.Flags().StringVar(&cfg.ClientOpts.AuthToken, "argocd-auth-token", "", "use token for authenticating to ArgoCD (unsafe - consider setting ARGOCD_TOKEN env var instead)")
+ rootCmd.Flags().BoolVar(&cfg.DryRun, "dry-run", false, "run in dry-run mode. If set to true, do not perform any changes")
+ rootCmd.Flags().DurationVar(&cfg.CheckInterval, "interval", 2*time.Minute, "interval for how often to check for updates")
+ rootCmd.Flags().StringVar(&cfg.LogLevel, "loglevel", "info", "set the loglevel to one of trace|debug|info|warn|error")
+ rootCmd.Flags().StringVar(&kubeConfig, "kubeconfig", "", "full path to kubernetes client configuration, i.e. ~/.kube/config")
+ rootCmd.Flags().IntVar(&cfg.HealthPort, "health-port", 8080, "port to start the health server on, 0 to disable")
+ rootCmd.Flags().BoolVar(&once, "once", false, "run only once, same as specifying --interval=0 and --healt-port=0")
+ rootCmd.Flags().StringVar(&cfg.RegistriesConf, "registries-conf-path", "", "path to registries configuration file")
+ rootCmd.Flags().BoolVar(&disableKubernetes, "disable-kubernetes", false, "do not create and use a Kubernetes client")
+
+ rootCmd.Flags().IntVar(&cfg.MaxConcurrency, "max-concurrency", 10, "maximum number of update threads to run concurrently")
+ rootCmd.Flags().StringVar(&cfg.ArgocdNamespace, "argocd-namespace", "argocd", "namespace where ArgoCD runs in")
+
+ err := rootCmd.Execute()
+ return err
+}
+
+func main() {
+ err := newCommand()
+ if err != nil {
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
diff --git a/config/example-config.yaml b/config/example-config.yaml
new file mode 100644
index 0000000..9879f02
--- /dev/null
+++ b/config/example-config.yaml
@@ -0,0 +1,37 @@
+# Example configuration for argocd-image-controller
+
+# Registry configuration. Each registry must have the following properties:
+#
+# name (string)
+# A name for the registry. Can be anything basically.
+# api_url (string)
+# API endpoint URL
+# prefix: (string)
+# The prefix for images from this registry
+# ping: (boolean)
+# Whether to perform a request on /v2 endpoint initially. Some registries
+# do not support this.
+# credentials: (string)
+# The credentials to use for the registry. See "specifying credentials" in
+# the documentation for string format.
+#
+# Exactly one registry can be specified without a prefix, which is then used
+# as the default registry for images. Most likely, this will be Docker Hub.
+#
+# Each prefix can be specified exactly once. There must not be more than one
+# registry with the same prefix configured.
+registries:
+- name: Docker Hub
+ api_url: https://registry-1.docker.io
+ ping: yes
+ credentials: env:SOME_ENV_VAR
+- name: Google Container Registry
+ api_url: https://gcr.io
+ prefix: gcr.io
+ ping: no
+ credentials: pullsecret:foo/bar
+- name: RedHat Quay
+ api_url: https://quay.io
+ ping: no
+ prefix: quay.io
+ credentials: secret:foo/bar#creds
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..da3ee49
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,3 @@
+# Placeholder for docs
+
+At some point in time, here the documentation will live.
diff --git a/docs/assets/extra.css b/docs/assets/extra.css
new file mode 100644
index 0000000..055aff2
--- /dev/null
+++ b/docs/assets/extra.css
@@ -0,0 +1,10 @@
+.codehilite {
+ background-color: hsla(0,0%,92.5%,.5);
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.codehilite pre {
+ background-color: transparent;
+ padding: .525rem .6rem;
+}
diff --git a/docs/assets/logo.png b/docs/assets/logo.png
new file mode 100644
index 0000000..b9cb18e
--- /dev/null
+++ b/docs/assets/logo.png
Binary files differ
diff --git a/docs/configuration/applications.md b/docs/configuration/applications.md
new file mode 100644
index 0000000..cc7165e
--- /dev/null
+++ b/docs/configuration/applications.md
@@ -0,0 +1,47 @@
+# Application configuration
+
+In order for ArgoCD Image Updater to know which applications it should inspect
+for updating the workloads' container images, the corresponding Kubernetes
+resource needs to be correctly annotated. ArgoCD Image Updater will inspect
+only resources of kind `application.argoproj.io`, that is, your ArgoCD
+`Application` resources. Annotations on other kinds of resources will have no
+effect and will not be considered.
+
+For its annotations, ArgoCD Image Updater uses the following prefix:
+
+```yaml
+argocd-image-updater.argoproj.io
+```
+
+As explained earlier, your ArgoCD applications must be of either `Kustomize`
+or `Helm` type. Other types of applications will be ignored.
+
+So, in order for ArgoCD Image Updater to consider your application for the
+update of its images, at least the following criteria must be met:
+
+* Your `Application` resource is annotated with the mandatory annotation of
+ `argocd-image-updater.argoproj.io/image-list`, which contains at least one
+ valid image specification (see [Images Configuration](images.md)).
+
+* Your `Application` resource is of type `Helm` or `Kustomize`
+
+An example of a correctly annotated `Application` resources might look like:
+
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ annotations:
+ argocd-image-updater.argoproj.io/image-list: gcr.io/heptio-images/ks-guestbook-demo:^0.1
+ name: guestbook
+ namespace: argocd
+spec:
+ destination:
+ namespace: guestbook
+ server: https://kubernetes.default.svc
+ project: default
+ source:
+ path: helm-guestbook
+ repoURL: https://github.com/argocd-example-apps/argocd-example-apps
+ targetRevision: HEAD
+```
diff --git a/docs/configuration/basics.md b/docs/configuration/basics.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/configuration/basics.md
diff --git a/docs/configuration/images.md b/docs/configuration/images.md
new file mode 100644
index 0000000..74c62c5
--- /dev/null
+++ b/docs/configuration/images.md
@@ -0,0 +1,120 @@
+# Configuring images for update
+
+## Annotation format
+
+You can specify one or more image(s) for each application that should be
+considered for updates. To specify those images, the following annotation
+is used:
+
+```yaml
+argocd-image-updater.argoproj.io/image-list: <image spec list>
+```
+
+The `<image spec list>` is a comma separated list of image specifications. Each
+image specification is composed of mandatory and optional information, and is
+used to specify the image, its version constraint and a few meta data.
+
+An image specification could be formally described as:
+
+```text
+[<image_name>=]<image_path>[:<version_constraint>][#<secret ref>]
+```
+
+Specifying the fields denoted in square brackets is optional and can be left
+out.
+
+## Allowing an image for update
+
+The most simple form of specifying an image allowed to update would be the
+following:
+
+```yaml
+argocd-image-updater.argoproj.io/image-list: nginx
+```
+
+The above example would specify to update the image `nginx` to it's most recent
+version found in the container registry, without taking any version constraints
+into cosideration.
+
+This is most likely not what you want, because you could pull in some breaking
+changes when `nginx` releases a new major version and the image gets updated.
+So you can give a version constraint along with the image specification:
+
+```yaml
+argocd-image-updater.argoproj.io/image-list: nginx:~1.26
+```
+
+The above example would allow the `nginx` image to be updated to any patch
+version within the `1.26` minor release.
+
+More information on how to specify semantic version constraints can be found
+in the
+[documentation](https://github.com/Masterminds/semver#checking-version-constraints)
+of the [Semver library](https://github.com/Masterminds/semver) we're using.
+
+## Naming images
+
+Giving a name to an image is necessary in these scenarios:
+
+* If you want to use custom images with Kustomize. In this case, the name must
+ match to what is defined in your Kustomize base.
+
+* If you need to specify the Helm parameters used for rendering the image name
+ and version using Helm and the parameter names do not equal `image.name` and
+ `image.tag`. In this case, the name is just symbolic.
+
+### Custom images with Kustomize
+
+In Kustomize, if you want to use an image from another registry or a completely
+different image than what is specified in the manifests, you can give the image
+specification as follows:
+
+```text
+<image_name>=<image_path>:<image_tag>
+```
+
+`<image_name>` will be the original image name, as used in your manifests, and
+`<image_path>:<image_path>` will be the value used when rendering the
+manifests.
+
+Let's take ArgoCD's Kustomize base as an example: The original image used by
+ArgoCD is `argoproj/argocd`, pulled from the Docker Hub container registry. If
+you are about to follow the latest builds, as published on the GitHub registry,
+you could override the image specification in Kustomize as follows:
+
+```text
+argoproj/argocd=docker.pkg.github.com/argoproj/argo-cd/argocd:1.7.0-a6399e59
+```
+
+### Specifying Helm parameter names
+
+!!!note
+ Image names should not be too complex. In case of Helm, they must only
+ consist of letters and numbers because the names will be reused in
+ Kubernetes annotation names, and thus, must fit in the overall naming
+ convention of Kubernetes annotation names.
+
+In case of Helm applications which contain more than one image in the manifests
+or use another set of parameters than `image.name` and `image.tag` to define
+which image to render in the manifests, you can use the `<name>` parameter in
+the image specification to define a (symbolic) name for that image. Then, you
+can use another set of annotations to specify the appropriate parameter names
+that should get set if an image gets updated.
+
+For example, if you have an image `quay.io/dexidp/dex` that is configured in
+your helm chart using the `dex.image.name` and `dex.image.tag` Helm parameters,
+you can set the following annotations on your `Application` resource so that
+ArgoCD Image Updater will know which Helm parameters to set:
+
+```yaml
+argocd-image-updater.argoproj.io/image-list: dex=quay.io/dexidp/dex
+argocd-image-updater.argoproj.io/dex.image-name: dex.image.name
+argocd-image-updater.argoproj.io/dex.image-tag: dex.image.tag
+
+```
+
+The general syntax for the two Helm specific annotations is:
+
+```yaml
+argocd-image-updater.argoproj.io/<name>.image-name: <name of helm parameter to set>
+``` \ No newline at end of file
diff --git a/docs/configuration/registries.md b/docs/configuration/registries.md
new file mode 100644
index 0000000..9d1dab9
--- /dev/null
+++ b/docs/configuration/registries.md
@@ -0,0 +1,94 @@
+# Configuring Container Registries
+
+ArgoCD Image Updater comes with support for the following registries out of the
+box:
+
+* Docker Hub Registry
+* Google Container Registry
+* RedHat Quay Registry
+
+Adding additional (and custom) container registries is supported by means of a
+configuration file. If you run ArgoCD Image Updater within Kubernetes, you can
+edit the registries in a ConfigMap resource, which will get mounted to the pod
+running ArgoCD Image Updater.
+
+## Configuring a custom container registry
+
+A sample configuration configuring a couple of registries might look like the
+following:
+
+```yaml
+registries:
+- name: Docker Hub
+ api_url: https://registry-1.docker.io
+ ping: yes
+ credentials: secret:foo/bar#creds
+- name: Google Container Registry
+ api_url: https://gcr.io
+ prefix: gcr.io
+ ping: no
+ credentials: pullsecret:foo/bar
+- name: RedHat Quay
+ api_url: https://quay.io
+ ping: no
+ prefix: quay.io
+ credentials: env:REGISTRY_SECRET
+```
+
+The above example defines access to three registries. The properties have the
+following semantics:
+
+* `name` is just a symbolic name for the registry. Must be unique.
+* `api_url` is the base URL (without `/v2` suffix) to the API of the registry
+* `ping` specifies whether to send a ping request to `/v2` endpoint first.
+ Some registries don't support this.
+* `prefix` is the prefix used in the image specification. This prefix will
+ be consulted when determining the configuration for given image(s).
+* `credentials` is a reference to the credentials to use for accessing the
+ registry API (see below)
+
+If you want to take above example to the `argocd-image-updater-cm` ConfigMap,
+you need to define the key `registries.conf` in the data of the ConfigMap as
+below:
+
+```yaml
+data:
+ registries.conf: |
+ registries:
+ - name: Docker Hub
+ api_url: https://registry-1.docker.io
+ ping: yes
+ credentials: secret:foo/bar#creds
+ - name: Google Container Registry
+ api_url: https://gcr.io
+ prefix: gcr.io
+ ping: no
+ credentials: pullsecret:foo/bar
+ - name: RedHat Quay
+ api_url: https://quay.io
+ ping: no
+ prefix: quay.io
+ credentials: env:REGISTRY_SECRET
+```
+
+!!!note
+ ArgoCD Image Updater pod must be restarted for changes to the registries
+ configuration to take effect. There are plans to change this behaviour so
+ that changes will be reload automatically in a future release.
+
+## Specifying credentials for accessing container registries
+
+You can optionally specify a reference to a secret or an environment variable
+which contain credentials for accessing the container registry with each image.
+
+Credentials can be referenced as follows:
+
+* A typical pull secret, i.e. a secret containing a `.dockerconfigjson` field
+ which holds a Docker client configuration with auth information in JSON
+ format.
+
+* A custom secret, which has the credentials stored in a configurable field in
+ the format `<username>:<password>`
+
+* An environment variable which holds the credentials in the format
+ `<username>:<password>`
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..87a806e
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,67 @@
+# ArgoCD Image Updater
+
+A tool to automatically update the container images of Kubernetes workloads
+that are managed by
+[ArgoCD](https://github.com/argoproj/argo-cd).
+
+!!!warning "A note on the current status"
+ ArgoCD Image Updater was born just recently. It is not suitable for
+ production use yet, and it might break things in unexpected ways.
+
+ You are welcome to test it out on non-critical environments, and to
+ contribute by sending bug reports, enhancement requests and - most
+ appreciated - pull requests.
+
+ There will be (probably a lot of) breaking changes from release to
+ release as development progresses until version 1.0. We will do our
+ best to indicate any breaking change and how to un-break it in the
+ [Changelog](https://github.com/argoproj-labs/argocd-image-updater/CHANGELOG.md)
+
+## Overview
+
+The ArgoCD Image Updater can check for new versions of the container images
+that are deployed with your Kubernetes workloads and automatically update them
+to their latest allowed version using ArgoCD. It works by setting appropriate
+application parameters for ArgoCD applications, i.e. similar to
+`argocd app set --helm-set image.tag=v1.0.1` - but in a fully automated
+manner.
+
+Usage is simple: You annotate your ArgoCD `Application` resources with a list
+of images to be considered for update, along with a version constraint to
+restrict the maximum allowed new version for each image. ArgoCD Image Updater
+then regulary polls the configured applications from ArgoCD and queries the
+corresponding container registry for possible new versions. If a new version of
+the image is found in the registry, and the version constraint is met, ArgoCD
+Image Updater instructs ArgoCD to update the application with the new image.
+
+Depending on your Automatic Sync Policy for the Application, ArgoCD will either
+automatically deploy the new image version or mark the Application as Out Of
+Sync, and you can trigger the image update manually by syncing the Application.
+Due to the tight integration with ArgoCD, advanced features like Sync Windows,
+RBAC authorization on Application resources etc. are fully supported.
+
+## Limitations
+
+The three most important limitations first. These will most likely not change
+anywhere in the near future, because they are limitations by design.
+
+Please make sure to understand these limitations, and do not send enhancement
+requests or bug reports related to the following:
+
+* The applications you want container images to be updated **must** be managed
+ using ArgoCD. There is no support for workloads not managed using ArgoCD.
+
+* ArgoCD Image Updater can only update container images for applications whose
+ manifests are rendered using either *Kustomize* or *Helm* and - especially
+ in the case of Helm - the templates need to support specifying the image's
+ tag (and possibly name) using a parameter (i.e. `image.tag`).
+
+* Your images' tags need to follow the semantic versioning scheme. ArgoCD
+ Image Updater will not be able to update images that are just made from
+ arbitrary strings, or consist solely of Git SHA strings.
+
+Otherwise, current known limitations are:
+
+* Image pull secrets must exist in the same Kubernetes cluster where ArgoCD
+ Image Updater is running in (or has accesst to). It is currently not possible
+ to fetch those secrets from other clusters.
diff --git a/docs/install/start.md b/docs/install/start.md
new file mode 100644
index 0000000..326f85f
--- /dev/null
+++ b/docs/install/start.md
@@ -0,0 +1,158 @@
+# Getting Started
+
+## Runtime environment
+
+It is recommend to run ArgoCD Image Updater in the same Kubernetes cluster that
+ArgoCD is running in, however, this is not a requirement. In fact, it is not
+even a requirement to run ArgoCD Image Updater within a Kubernetes cluster or
+with access to any Kubernetes cluster at all.
+
+However, some features might not work without accessing Kubernetes.
+
+## Prerequisites
+
+ArgoCD Image Updater will need access to the API of your ArgoCD installation.
+If you chose to install the ArgoCD Image Updater outside of the cluster where
+ArgoCD is running in, the API must be exposed externally (i.e. using Ingress).
+If you have network policies in place, make sure that ArgoCD Image Updater will
+be allowed to communicate with the ArgoCD API, which is usually the service
+`argocd-server` in namespace `argocd` on port 443 and port 80.
+
+### Create a local user within ArgoCD
+
+ArgoCD Image Updater needs credential for accessing the ArgoCD API. Using a
+[local user](https://argoproj.github.io/argo-cd/operator-manual/user-management/)
+is recommended, but a *project token* will work as well (although, this will
+limit updating to the applications of the given project obviously).
+
+Let's use an account named `image-updater` with appropriate API permissions.
+
+Add the following user definition to `argocd-cm`:
+
+```yaml
+data:
+ # ...
+ accounts.image-updater: apiKey
+```
+
+Now, you will need to create an access token for this user, which can be either
+done using the CLI or the Web UI. The following CLI command will create a named
+token for the user and print it to the console:
+
+```shell
+argocd account generate-token --account image-updater --id image-updater
+```
+
+Copy the token's value somewhere, you will need it later on.
+
+### Granting RBAC permissions in ArgoCD
+
+The technical user `image-updater` we have configured in the previous step now
+needs appropriate RBAC permissions within ArgoCD. ArgoCD Image Updater needs
+the `update` and `get` permissions on the applications you want to manage.
+
+A most basic version that grants `get` and `update` permissions on all of the
+applications managed by ArgoCD might look as follows:
+
+```text
+p, role:image-updater, applications, get, */*, allow
+p, role:image-updater, applications, update, */*, allow
+g, image-updater, role:image-updater
+```
+
+You might want to strip that down to apps in a specific project, or to specific
+apps, however.
+
+Put the RBAC permissions to ArgoCD's `argocd-rbac-cm` ConfigMap and ArgoCD will
+pick them up automatically.
+
+## Installing as Kubernetes workload
+
+Installation is straight-forward. Don't worry, without any configuration, it
+will not start messing with your workloads yet.
+
+!!!note
+ We also provide a Kustomize base in addition to the plain Kubernetes YAML
+ manifests. You can use it as remote base and create overlays with your
+ configuration on top of it. The remote base's URL is
+ `https://github.com/argoproj-labs/argocd-image-updater/manifests/base`
+
+### Create a dedicated namespace for ArgoCD Image Updater
+
+```shell
+kubectl create ns argocd-image-updater`
+```
+
+### Apply the installation manifests
+
+```shell
+kubectl apply -n argocd-image-updater -f manifests/install.yaml
+```
+
+!!!note "A word on high availabilty"
+ It is not advised to run multiple replicas of the same ArgoCD Image Updater
+ instance. Just leave the number of replicas at 1, otherwise weird side
+ effects could occur.
+
+### Configure API access token secret
+
+When installed from the manifests into a Kubernetes cluster, the ArgoCD Image
+Updater reads the token required for accessing ArgoCD API from an environment
+variable named `ARGOCD_TOKEN`, which is set from a a field named
+`argocd.token` in a secret named `argocd-image-updater-secret`.
+
+The value for `argocd.token` should be set to the *base64 encoded* value of the
+access token you have generated above. As a short-cut, you can use generate the
+secret with `kubectl` and apply it over the existing resource:
+
+```shell
+kubectl create secret generic argocd-image-updater-secret \
+ --from-literal argocd.token=$YOUR_TOKEN --dry-run -o yaml |
+ kubectl -n argocd-image-updater apply -f -
+```
+
+You must restart the `argocd-image-updater` pod after such a change, i.e run
+
+```shell
+kubectl rollout restart deployment argocd-image-updater
+```
+
+Or alternatively, simply delete the running pod to have it recreated by
+Kubernetes automatically.
+
+## Running locally
+
+As long as you have access to the ArgoCD API and your Kubernetes cluster from
+your workstation, running ArgoCD Image Updater is simple. Make sure that you
+have your API token noted and that your Kubernetes client configuration points
+to the correct K8s cluster.
+
+Grab the binary (it does not have any external dependencies) and run:
+
+```bash
+export ARGOCD_TOKEN=<yourtoken>
+./argocd-image-updater \
+ --kubeconfig ~/.kube/config
+ --argocd-server-addr argo-cd.example.com
+ --once
+```
+
+Note: The `--once` flag disables the health server and the check interval, so
+the tool will not regulary check for updates but exit after the first run.
+
+Check `argocd-image-updater --help` for a list of valid command line flags, or
+consult the appropriate section of the documentation.
+
+## Running multiple instances
+
+Generally, multiple instances of ArgoCD Image Updater can be run within the same
+Kubernetes cluster, however they should not operate on the same set of
+applications. This allows for multiple application teams to manage their own set
+of applications.
+
+If opting for such an approach, you should make sure that:
+
+* Each instance of ArgoCD Image Updater runs in its own namespace
+* Each instance has a dedicated user in ArgoCD, with dedicated RBAC permissions
+* RBAC permissions are set-up so that instances cannot interfere with each
+ others managed resources
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..64c3da1
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,56 @@
+module github.com/argoproj-labs/argocd-image-updater
+
+go 1.14
+
+require (
+ github.com/Masterminds/semver v1.5.0
+ github.com/argoproj/argo-cd v1.6.2
+ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
+ github.com/gorilla/mux v1.7.4 // indirect
+ github.com/nokia/docker-registry-client v0.0.0-20190305095957-e91f10057c5b
+ github.com/sirupsen/logrus v1.6.0
+ github.com/spf13/cobra v1.0.0
+ github.com/stretchr/testify v1.6.1
+ gopkg.in/yaml.v2 v2.2.8
+ k8s.io/api v1.17.8
+ k8s.io/apiextensions-apiserver v1.17.8 // indirect
+ k8s.io/apimachinery v1.17.8
+ k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible
+ k8s.io/klog/v2 v2.3.0 // indirect
+ k8s.io/kubectl v1.17.8 // indirect
+ k8s.io/kubernetes v1.17.8 // indirect
+ k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 // indirect
+)
+
+replace (
+ github.com/golang/protobuf => github.com/golang/protobuf v1.3.2
+ github.com/grpc-ecosystem/grpc-gateway => github.com/grpc-ecosystem/grpc-gateway v1.9.5
+ github.com/improbable-eng/grpc-web => github.com/improbable-eng/grpc-web v0.0.0-20181111100011-16092bd1d58a
+
+ google.golang.org/grpc => google.golang.org/grpc v1.15.0
+
+ k8s.io/api => k8s.io/api v0.17.8
+ k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.17.8
+ k8s.io/apimachinery => k8s.io/apimachinery v0.17.8
+ k8s.io/apiserver => k8s.io/apiserver v0.17.8
+ k8s.io/cli-runtime => k8s.io/cli-runtime v0.17.8
+ k8s.io/client-go => k8s.io/client-go v0.17.8
+ k8s.io/cloud-provider => k8s.io/cloud-provider v0.17.8
+ k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.17.8
+ k8s.io/code-generator => k8s.io/code-generator v0.17.8
+ k8s.io/component-base => k8s.io/component-base v0.17.8
+ k8s.io/cri-api => k8s.io/cri-api v0.17.8
+ k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.17.8
+ k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.17.8
+ k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.17.8
+ k8s.io/kube-proxy => k8s.io/kube-proxy v0.17.8
+ k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.17.8
+ k8s.io/kubectl => k8s.io/kubectl v0.17.8
+ k8s.io/kubelet => k8s.io/kubelet v0.17.8
+ k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.17.8
+ k8s.io/metrics => k8s.io/metrics v0.17.8
+ k8s.io/node-api => k8s.io/node-api v0.17.8
+ k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.17.8
+ k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.17.8
+ k8s.io/sample-controller => k8s.io/sample-controller v0.17.8
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a8f96cc
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,946 @@
+bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1:Ulb78X89vxKYgdL24HMTiXYHlyHEvruOj1ZPlqeNEZM=
+bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+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/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+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/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
+github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+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/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14=
+github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU=
+github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
+github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg=
+github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d/go.mod h1:WML6KOYjeU8N6YyusMjj2qRvaPNUEvrQvaxuFcMRFJY=
+github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+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/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
+github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
+github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/argoproj/argo-cd v1.6.2 h1:kpoS3TxMJYkUmtri6sXe1QbyjGkC9OaHT43K1B/8y6E=
+github.com/argoproj/argo-cd v1.6.2/go.mod h1:VHSJfpnOXUjtaDyb4C34YPO4pnZ79vRqa5nDeqh3PO8=
+github.com/argoproj/gitops-engine v0.1.3 h1:eQp1bfqaeaATcu4XErlxNb6aVsN4rC7suL/Fqx/9E+k=
+github.com/argoproj/gitops-engine v0.1.3/go.mod h1:UmBGlQLT/MPNiMmbnouZRWhkk3slPuozMsENdXMkIMs=
+github.com/argoproj/pkg v0.0.0-20200102163130-2dd1f3f6b4de/go.mod h1:2EZ44RG/CcgtPTwrRR0apOc7oU6UIw8GjCUJWZ8X3bM=
+github.com/argoproj/pkg v0.0.0-20200319004004-f46beff7cd54 h1:hDn02iEkh5EUl4TJfOo6AI9uSgh0vt/qh66ODuQl/YE=
+github.com/argoproj/pkg v0.0.0-20200319004004-f46beff7cd54/go.mod h1:2EZ44RG/CcgtPTwrRR0apOc7oU6UIw8GjCUJWZ8X3bM=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM=
+github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/bazelbuild/bazel-gazelle v0.18.2/go.mod h1:D0ehMSbS+vesFsLGiD6JXu3mVEzOlfUl8wNnq+x/9p0=
+github.com/bazelbuild/bazel-gazelle v0.19.1-0.20191105222053-70208cbdc798/go.mod h1:rPwzNHUqEzngx1iVBfO/2X2npKaT3tqPqqHW6rVsn/A=
+github.com/bazelbuild/buildtools v0.0.0-20190731111112-f720930ceb60/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=
+github.com/bazelbuild/buildtools v0.0.0-20190917191645-69366ca98f89/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=
+github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=
+github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17OJKJXD2Cfs=
+github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/bsm/redislock v0.4.3/go.mod h1:mcygIsJknQThqWrlOgiPJ97CGmu3aAdQabg1ZIxT1BA=
+github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E=
+github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog=
+github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/cespare/prettybench v0.0.0-20150116022406-03b8cfe5406c/go.mod h1:Xe6ZsFhtM8HrDku0pxJ3/Lr51rwykrzgFwpmTzleatY=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
+github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 h1:HD4PLRzjuCVW79mQ0/pdsalOLHJ+FaEoqJLxfltpb2U=
+github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
+github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho=
+github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/cfssl v0.0.0-20180726162950-56268a613adf/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
+github.com/clusterhq/flocker-go v0.0.0-20160920122132-2b8b7259d313/go.mod h1:P1wt9Z3DP8O6W3rvwCt0REIlshg1InHImaLW0t3ObY0=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
+github.com/container-storage-interface/spec v1.1.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4=
+github.com/container-storage-interface/spec v1.2.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4=
+github.com/containerd/console v0.0.0-20170925154832-84eeaae905fa/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/coredns/corefile-migration v1.0.2/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E=
+github.com/coredns/corefile-migration v1.0.4/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
+github.com/coreos/go-oidc v2.1.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-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/rkt v1.30.0/go.mod h1:O634mlH6U7qk87poQifK6M2rsFNt+FyUTWNMnP1hF1U=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
+github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
+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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v1.6.0-rc5 h1:8dnqiCOcZf2QXwR4LNnG7AK9hXeeT6adGmtjicsVswc=
+github.com/docker/docker v1.6.0-rc5/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.3.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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8=
+github.com/docker/libnetwork v0.8.0-dev.2.0.20190624125649-f0e46a78ea34/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8=
+github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
+github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s=
+github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+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/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
+github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
+github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw=
+github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
+github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
+github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
+github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
+github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
+github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+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/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
+github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
+github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
+github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
+github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
+github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
+github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
+github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
+github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
+github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
+github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
+github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
+github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
+github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
+github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
+github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
+github.com/go-redis/cache v6.3.5+incompatible h1:4OUyoXXYRRQ6tKA4ue3TlPUkBzk3occzjtXBZBxCzgs=
+github.com/go-redis/cache v6.3.5+incompatible/go.mod h1:XNnMdvlNjcZvHjsscEozHAeOeSE5riG9Fj54meG4WT4=
+github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
+github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
+github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
+github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
+github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
+github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
+github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
+github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
+github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
+github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
+github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
+github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
+github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
+github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
+github.com/gobuffalo/packr v1.11.0/go.mod h1:rYwMLC6NXbAbkKb+9j3NTKbxSswkKLlelZYccr4HYVw=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/gogits/go-gogs-client v0.0.0-20190616193657-5a05380e4bc2/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
+github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
+github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
+github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
+github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
+github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
+github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
+github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
+github.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg=
+github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
+github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
+github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
+github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
+github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
+github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
+github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
+github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
+github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
+github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8=
+github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk=
+github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
+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/cadvisor v0.34.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48=
+github.com/google/cadvisor v0.35.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48=
+github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
+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/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI=
+github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
+github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
+github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/heketi/heketi v9.0.1-0.20190917153846-c2e2a4ab7ab9+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o=
+github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/improbable-eng/grpc-web v0.0.0-20181111100011-16092bd1d58a/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+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/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/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.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
+github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
+github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA=
+github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04=
+github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk=
+github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao=
+github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
+github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/malexdev/utfutil v0.0.0-20180510171754-00c8d4a8e7a8/go.mod h1:UtpLyb/EupVKXF/N0b4NRe1DNg+QYJsnsHQ038romhM=
+github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4=
+github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
+github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mindprince/gonvml v0.0.0-20171110221305-fee913ce8fb2/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY=
+github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY=
+github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
+github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+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/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
+github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
+github.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
+github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
+github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
+github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
+github.com/nokia/docker-registry-client v0.0.0-20190305095957-e91f10057c5b h1:6d02Onq/KxC2qZlMzSwLx12KZU80xIS7hRQw05/nDJs=
+github.com/nokia/docker-registry-client v0.0.0-20190305095957-e91f10057c5b/go.mod h1:0DpUaZpSvIXrsvYc6Wb+fKwjhKz0Lu1NHwMziqTqqvA=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
+github.com/onsi/gomega v1.7.0/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 v1.0.0-rc2.0.20190611121236-6cc515888830/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/selinux v1.2.2/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
+github.com/opencontainers/selinux v1.3.1-0.20190929122143-5215b1806f52/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
+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/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
+github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/pquerna/cachecontrol v0.0.0-20180306154005-525d0eb5f91d h1:7gXyC293Lsm2YWgQ+0uaAFFFDO82ruiQSwc3ua+Vtlc=
+github.com/pquerna/cachecontrol v0.0.0-20180306154005-525d0eb5f91d/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
+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/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+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-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
+github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI=
+github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
+github.com/robfig/cron v1.1.0 h1:jk4/Hud3TTdcrJgUOBgsqrZBarcxl6ADIjSC2iniwLY=
+github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
+github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
+github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=
+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 v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
+github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
+github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
+github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
+github.com/vmihailenco/msgpack v3.3.1+incompatible h1:ibe+d1lqocBmxbJ+gwcDO8LpAHFr3PGDYovoURuTVGk=
+github.com/vmihailenco/msgpack v3.3.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/vmware/govmomi v0.20.1/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
+github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
+github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yudai/gojsondiff v1.0.1-0.20180504020246-0525c875b75c h1:vGHScYm0uhmaxwGX38tj1TB1u1zVdO0vlgcz1fEVxc8=
+github.com/yudai/gojsondiff v1.0.1-0.20180504020246-0525c875b75c/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
+github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
+github.com/yuin/gopher-lua v0.0.0-20190115140932-732aa6820ec4 h1:1yOVVSFiradDwXpgdkDjlGOcGJqcohH/W49Zn8Ywgco=
+github.com/yuin/gopher-lua v0.0.0-20190115140932-732aa6820ec4/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20190927031335-2835ba2e683f/go.mod h1:fYw7AShPAhGMdXqA9gRadk/CcMsvLlClpE5oBwnS3dM=
+golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
+golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190122202912-9c309ee22fab/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
+gonum.org/v1/gonum v0.0.0-20190621125449-90b715451587/go.mod h1:03dgh78c4UvU1WksguQ/lvJQXbezKQGJSrwwRq5MraQ=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw=
+google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+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/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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/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/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/go-playground/webhooks.v5 v5.11.0/go.mod h1:LZbya/qLVdbqDR1aKrGuWV6qbia2zCYSR5dpom2SInQ=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
+gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
+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/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+k8s.io/api v0.17.8 h1:8JHlbqJ3A6sGhoacXfu/sASSD+HWWqVq67qt9lyB0kU=
+k8s.io/api v0.17.8/go.mod h1:N++Llhs8kCixMUoCaXXAyMMPbo8dDVnh+IQ36xZV2/0=
+k8s.io/apiextensions-apiserver v0.17.8 h1:/E4h3wlnhdanffd/WzVJYd86I0fj76+4OPoHooAyHDI=
+k8s.io/apiextensions-apiserver v0.17.8/go.mod h1:5H/i0XiKizIE9SkoAQaU/ou31JJBIffbsT0ALA18GmE=
+k8s.io/apimachinery v0.17.8 h1:zXvd8rYMAjRJXpILP9tdAiUnFIENM9EmHuE81apIoms=
+k8s.io/apimachinery v0.17.8/go.mod h1:Lg8zZ5iC/O8UjCqW6DNhcQG2m4TdjF9kwG3891OWbbA=
+k8s.io/apiserver v0.17.8 h1:bazdS/BsMOo4SOh+EueJ0s34A1oHF+BQptI3+Dx9d3A=
+k8s.io/apiserver v0.17.8/go.mod h1:XU2YBi1I/v/P1R5lb0lEwSQ1rnXE01k7yxVtdIWH4Lo=
+k8s.io/cli-runtime v0.17.8 h1:h3igIXMRqLWFReByj0f1kwyk0hXz26uFt8KPUilZLSo=
+k8s.io/cli-runtime v0.17.8/go.mod h1:YDS2GZU0dhHUPIh1tjex69MhR9Gt7//LqDN+XR4vbaA=
+k8s.io/client-go v0.17.8 h1:cuZSfjqVrNjoZ3wViQHljFPyWMOcgxUjjmQs5Rifbxk=
+k8s.io/client-go v0.17.8/go.mod h1:SJsDS64AAtt9VZyeaQMb4Ck5etCitZ/FwajWdzua5eY=
+k8s.io/cloud-provider v0.17.8/go.mod h1:mWDJQa74syUaNR01TD5+fpfA9wGGEdAmMZ28+gZsWpk=
+k8s.io/cluster-bootstrap v0.17.8/go.mod h1:SC9J2Lt/MBOkxcCB04+5mYULLfDQL5kdM0BjtKaVCVU=
+k8s.io/code-generator v0.17.8/go.mod h1:iiHz51+oTx+Z9D0vB3CH3O4HDDPWrvZyUgUYaIE9h9M=
+k8s.io/component-base v0.17.8 h1:3YilgRh9TcifVsKWReiZL1JfoUzqLesDc0wYIpimJN8=
+k8s.io/component-base v0.17.8/go.mod h1:xfNNdTAMsYzdiAa8vXnqDhRVSEgkfza0iMt0FrZDY7s=
+k8s.io/cri-api v0.17.8/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
+k8s.io/csi-translation-lib v0.17.8/go.mod h1:+t8mboEYMOF7F9rUVisH6WTInUputoR5mCdFPBKM3dg=
+k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM=
+k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
+k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.3.0 h1:WmkrnW7fdrm0/DMClc+HIxtftvxVIPAhlVwMQo5yLco=
+k8s.io/klog/v2 v2.3.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-aggregator v0.17.8 h1:F84tBo1sMj61/MhEaWczcbodWIgFAUfCVFIx5w8ZmQY=
+k8s.io/kube-aggregator v0.17.8/go.mod h1:26iremCqRYiHc1arbu189DBDUCFxn2pvr0ybu46/Jh0=
+k8s.io/kube-controller-manager v0.17.8/go.mod h1:VoiS0KHxLD6Bme5o6Vg8IvIsvLx1ZFqehdjlqFOC3VM=
+k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29 h1:NeQXVJ2XFSkRoPzRo8AId01ZER+j8oV4SZADT4iBOXQ=
+k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29/go.mod h1:F+5wygcW0wmRTnM3cOgIqGivxkwSWIWT5YdsDbeAOaU=
+k8s.io/kube-proxy v0.17.8/go.mod h1:UGWr3EM5xtzhQuay5QRoP2eev7WoS7cPtrTmdhUcQHg=
+k8s.io/kube-scheduler v0.17.8/go.mod h1:5PmMQbjx2xPHlKBZ3tOMDBdaluZzj0W17INszU//wDs=
+k8s.io/kubectl v0.17.8 h1:SI8A/8X4KRmbOqOUXASP3DTTQeaUQG+WZUd3VlZ884k=
+k8s.io/kubectl v0.17.8/go.mod h1:6VzasxnHbJE6gilDM26QZVXuG52nUQLEK3IqPg0VCZY=
+k8s.io/kubelet v0.17.8/go.mod h1:mC9lqmZVHnn3XLsIUPwwm4PtT0tZLqdWnkMsKIweZ8k=
+k8s.io/kubernetes v1.16.6 h1:ZWSNwxZ1w/IPV7pYH9gohR7AhKmn1VoJ9fEKxmkkeh8=
+k8s.io/kubernetes v1.16.6/go.mod h1:rO6tSgbJjbo6lLkrq4jryUaXqZ2PdDJjzWXKZQmLfnQ=
+k8s.io/kubernetes v1.17.8 h1:VKkgXDo0PzBeoVABOWc4/ozDOzoouHbNxljqXC8TRIg=
+k8s.io/kubernetes v1.17.8/go.mod h1:GpyQ+yngwJdtG0MRkt+xbAS7oBj3OZ4mIWIjy6SIjws=
+k8s.io/legacy-cloud-providers v0.17.8/go.mod h1:JXHJQnSV8TvCn5DLVMKVLXr1i1FfAqpob0KFSSd9Fxw=
+k8s.io/metrics v0.17.8/go.mod h1:Q19tjsPsAIhcnCvwqUwyrVT/NVZ4wmDuDllizdft9cw=
+k8s.io/repo-infra v0.0.1-alpha.1/go.mod h1:wO1t9WaB99V80ljbeENTnayuEEwNZt7gECYh/CEyOJ8=
+k8s.io/sample-apiserver v0.17.8/go.mod h1:65hWrOCGC2tKxhL3s44zpbG38W0RMDAo0KvgmOMzaXM=
+k8s.io/system-validators v1.0.4/go.mod h1:HgSgTg4NAGNoYYjKsUyk52gdNi2PVDswQ9Iyn66R7NI=
+k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+k8s.io/utils v0.0.0-20191114200735-6ca3b61696b6 h1:p0Ai3qVtkbCG/Af26dBmU0E1W58NID3hSSh7cMyylpM=
+k8s.io/utils v0.0.0-20191114200735-6ca3b61696b6/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 h1:7Nu2dTj82c6IaWvL7hImJzcXoTPz1MsSCH7r+0m6rfo=
+k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc=
+modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
+modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
+mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
+mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
+mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
+sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
+sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
+sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE=
+sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
+vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=
diff --git a/hack/generate-manifests.sh b/hack/generate-manifests.sh
new file mode 100755
index 0000000..27286d5
--- /dev/null
+++ b/hack/generate-manifests.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+set -eo pipefail
+set -x
+
+SRCROOT="$( CDPATH='' cd -- "$(dirname "$0")/.." && pwd -P )"
+KUSTOMIZE="kustomize"
+TEMPFILE=$(mktemp /tmp/aic-manifests.XXXXXX)
+
+IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-argoproj-labs}"
+IMAGE_TAG="${IMAGE_TAG:-}"
+
+# if the tag has not been declared, and we are on a release branch, use the VERSION file.
+if [ "$IMAGE_TAG" = "" ]; then
+ branch=$(git rev-parse --abbrev-ref HEAD || true)
+ if [[ $branch = release-* ]]; then
+ pwd
+ IMAGE_TAG=v$(cat $SRCROOT/VERSION)
+ fi
+fi
+# otherwise, use latest
+if [ "$IMAGE_TAG" = "" ]; then
+ IMAGE_TAG=latest
+fi
+
+cd ${SRCROOT}/manifests/base && kustomize edit set image argoproj-labs/argocd-image-updater=${IMAGE_NAMESPACE}/argocd-image-updater:${IMAGE_TAG}
+cd ${SRCROOT}/manifests/base && ${KUSTOMIZE} build . > ${TEMPFILE}
+
+mv ${TEMPFILE} ${SRCROOT}/manifests/install.yaml
+cd ${SRCROOT} && chmod 644 manifests/install.yaml
diff --git a/manifests/base/config/argocd-image-updater-cm.yaml b/manifests/base/config/argocd-image-updater-cm.yaml
new file mode 100644
index 0000000..34b6358
--- /dev/null
+++ b/manifests/base/config/argocd-image-updater-cm.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: argocd-image-updater-config
+ labels:
+ app.kubernetes.io/name: argocd-image-updater-config
+ app.kubernetes.io/part-of: argocd-image-updater
diff --git a/manifests/base/config/argocd-image-updater-secret.yaml b/manifests/base/config/argocd-image-updater-secret.yaml
new file mode 100644
index 0000000..80dc6e6
--- /dev/null
+++ b/manifests/base/config/argocd-image-updater-secret.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: argocd-image-updater-secret
+ labels:
+ app.kubernetes.io/name: argocd-image-updater-secret
+ app.kubernetes.io/part-of: argocd-image-updater
diff --git a/manifests/base/config/kustomization.yaml b/manifests/base/config/kustomization.yaml
new file mode 100644
index 0000000..9605515
--- /dev/null
+++ b/manifests/base/config/kustomization.yaml
@@ -0,0 +1,6 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- argocd-image-updater-cm.yaml
+- argocd-image-updater-secret.yaml \ No newline at end of file
diff --git a/manifests/base/deployment/argocd-image-updater-deployment.yaml b/manifests/base/deployment/argocd-image-updater-deployment.yaml
new file mode 100644
index 0000000..e019857
--- /dev/null
+++ b/manifests/base/deployment/argocd-image-updater-deployment.yaml
@@ -0,0 +1,55 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app.kubernetes.io/name: argocd-image-updater
+ app.kubernetes.io/part-of: argocd-image-updater
+ app.kubernetes.io/component: controller
+ name: argocd-image-updater
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: argocd-image-updater
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: argocd-image-updater
+ spec:
+ containers:
+ - image: argoproj-labs/argocd-image-updater:latest
+ imagePullPolicy: Always
+ env:
+ - name: ARGOCD_GRPC_WEB
+ valueFrom:
+ configMapKeyRef:
+ name: argocd-image-updater-config
+ key: argocd.grpc_web
+ optional: true
+ - name: ARGOCD_SERVER
+ valueFrom:
+ configMapKeyRef:
+ name: argocd-image-updater-config
+ key: argocd.server_addr
+ optional: true
+ - name: ARGOCD_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: argocd-image-updater-secret
+ key: argocd.token
+ optional: true
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8080
+ initialDelaySeconds: 3
+ periodSeconds: 30
+ name: argocd-image-updater
+ ports:
+ - containerPort: 8080
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: 8080
+ initialDelaySeconds: 3
+ periodSeconds: 30
+ serviceAccountName: argocd-image-updater
diff --git a/manifests/base/deployment/kustomization.yaml b/manifests/base/deployment/kustomization.yaml
new file mode 100644
index 0000000..79567d2
--- /dev/null
+++ b/manifests/base/deployment/kustomization.yaml
@@ -0,0 +1,5 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- argocd-image-updater-deployment.yaml \ No newline at end of file
diff --git a/manifests/base/kustomization.yaml b/manifests/base/kustomization.yaml
new file mode 100644
index 0000000..efc49e1
--- /dev/null
+++ b/manifests/base/kustomization.yaml
@@ -0,0 +1,13 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+
+images:
+- name: argoproj-labs/argocd-image-updater
+ newName: argoproj-labs/argocd-image-updater
+ newTag: latest
+
+resources:
+- ./config
+- ./deployment
+- ./rbac
diff --git a/manifests/base/rbac/argocd-image-updater-sa.yaml b/manifests/base/rbac/argocd-image-updater-sa.yaml
new file mode 100644
index 0000000..1fe10d9
--- /dev/null
+++ b/manifests/base/rbac/argocd-image-updater-sa.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ app.kubernetes.io/name: argocd-image-updater
+ app.kubernetes.io/part-of: argocd-image-updater
+ app.kubernetes.io/component: controller
+ name: argocd-image-updater
diff --git a/manifests/base/rbac/kustomization.yaml b/manifests/base/rbac/kustomization.yaml
new file mode 100644
index 0000000..1cfaefc
--- /dev/null
+++ b/manifests/base/rbac/kustomization.yaml
@@ -0,0 +1,5 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- argocd-image-updater-sa.yaml \ No newline at end of file
diff --git a/manifests/install.yaml b/manifests/install.yaml
new file mode 100644
index 0000000..5de6e41
--- /dev/null
+++ b/manifests/install.yaml
@@ -0,0 +1,80 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ app.kubernetes.io/component: controller
+ app.kubernetes.io/name: argocd-image-updater
+ app.kubernetes.io/part-of: argocd-image-updater
+ name: argocd-image-updater
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ labels:
+ app.kubernetes.io/name: argocd-image-updater-config
+ app.kubernetes.io/part-of: argocd-image-updater
+ name: argocd-image-updater-config
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ labels:
+ app.kubernetes.io/name: argocd-image-updater-secret
+ app.kubernetes.io/part-of: argocd-image-updater
+ name: argocd-image-updater-secret
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app.kubernetes.io/component: controller
+ app.kubernetes.io/name: argocd-image-updater
+ app.kubernetes.io/part-of: argocd-image-updater
+ name: argocd-image-updater
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: argocd-image-updater
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: argocd-image-updater
+ spec:
+ containers:
+ - env:
+ - name: ARGOCD_GRPC_WEB
+ valueFrom:
+ configMapKeyRef:
+ key: argocd.grpc_web
+ name: argocd-image-updater-config
+ optional: true
+ - name: ARGOCD_SERVER
+ valueFrom:
+ configMapKeyRef:
+ key: argocd.server_addr
+ name: argocd-image-updater-config
+ optional: true
+ - name: ARGOCD_TOKEN
+ valueFrom:
+ secretKeyRef:
+ key: argocd.token
+ name: argocd-image-updater-secret
+ optional: true
+ image: argoproj-labs/argocd-image-updater:latest
+ imagePullPolicy: Always
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8080
+ initialDelaySeconds: 3
+ periodSeconds: 30
+ name: argocd-image-updater
+ ports:
+ - containerPort: 8080
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: 8080
+ initialDelaySeconds: 3
+ periodSeconds: 30
+ serviceAccountName: argocd-image-updater
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..6911698
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,28 @@
+
+site_name: Argo CD Image Updater
+repo_url: https://github.com/argoproj-labs/argocd-image-updater
+strict: true
+theme:
+ name: material
+ palette:
+ primary: teal
+ font:
+ text: 'Work Sans'
+ logo: 'assets/logo.png'
+extra_css:
+ - 'assets/extra.css'
+markdown_extensions:
+- codehilite
+- admonition
+- toc:
+ permalink: true
+nav:
+ - Overview: index.md
+ - Install:
+ - Getting Started: install/start.md
+ - Configuration:
+ - Applications: configuration/applications.md
+ - Images: configuration/images.md
+ - Container Registries: configuration/registries.md
+ - Releases ⧉: https://github.com/argoproj-labs/argocd-image-updater/releases
+ - Roadmap ⧉: https://github.com/argoproj-labs/argocd-image-updater/milestones \ No newline at end of file
diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go
new file mode 100644
index 0000000..8ee07b1
--- /dev/null
+++ b/pkg/argocd/argocd.go
@@ -0,0 +1,365 @@
+package argocd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/common"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/image"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+
+ argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
+ "github.com/argoproj/argo-cd/pkg/apiclient/application"
+ "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+)
+
+type ArgoCD struct {
+ Client argocdclient.Client
+}
+
+// Interface that we need mocks for
+type ArgoCDClient interface {
+ ListApplications() ([]v1alpha1.Application, error)
+ SetHelmImage(appName string, newImage *image.ContainerImage) error
+ GetImagesFromApplication(appName string) (image.ContainerImageList, error)
+ GetApplicationTypeByName(appName string) (ApplicationType, error)
+}
+
+// Type of the application
+type ApplicationType int
+
+const (
+ ApplicationTypeUnsupported ApplicationType = 0
+ ApplicationTypeHelm ApplicationType = 1
+ ApplicationTypeKustomize ApplicationType = 2
+)
+
+// Basic wrapper struct for ArgoCD client options
+type ClientOptions struct {
+ ServerAddr string
+ Insecure bool
+ Plaintext bool
+ Certfile string
+ GRPCWeb bool
+ GRPCWebRootPath string
+ AuthToken string
+}
+
+// NewClient creates a new API client for ArgoCD and connects to the ArgoCD
+// API server.
+func NewClient(opts *ClientOptions) (*ArgoCD, error) {
+
+ envAuthToken := os.Getenv("ARGOCD_TOKEN")
+ if envAuthToken != "" && opts.AuthToken == "" {
+ opts.AuthToken = envAuthToken
+ }
+
+ rOpts := argocdclient.ClientOptions{
+ ServerAddr: opts.ServerAddr,
+ PlainText: opts.Plaintext,
+ Insecure: opts.Insecure,
+ CertFile: opts.Certfile,
+ GRPCWeb: opts.GRPCWeb,
+ GRPCWebRootPath: opts.GRPCWebRootPath,
+ AuthToken: opts.AuthToken,
+ }
+ client, err := argocdclient.NewClient(&rOpts)
+ if err != nil {
+ return nil, err
+ }
+ return &ArgoCD{Client: client}, nil
+}
+
+type ApplicationImages struct {
+ Application v1alpha1.Application
+ Images image.ContainerImageList
+}
+
+// Will hold a list of applications with the images allowed to considered for
+// update.
+type ImageList map[string]ApplicationImages
+
+// Retrieve a list of applications from ArgoCD that qualify for image updates
+// Application needs either to be of type Kustomize or Helm and must have the
+// correct annotation in order to be considered.
+func FilterApplicationsForUpdate(apps []v1alpha1.Application) (map[string]ApplicationImages, error) {
+ var appsForUpdate = make(map[string]ApplicationImages)
+
+ for _, app := range apps {
+ if !IsValidApplicationType(&app) {
+ log.Tracef("skipping app '%s' of type '%s' because it's not of supported source type", app.GetName(), app.Status.SourceType)
+ continue
+ }
+ annotations := app.GetAnnotations()
+ if updateImage, ok := annotations[common.ImageUpdaterAnnotation]; !ok {
+ log.Tracef("skipping app '%s' of type '%s' because required annotation is missing", app.GetName(), app.Status.SourceType)
+ continue
+ } else {
+ log.Tracef("processing app '%s' of type '%v'", app.GetName(), app.Status.SourceType)
+ imageList := make(image.ContainerImageList, 0)
+ for _, imageName := range strings.Split(updateImage, ",") {
+ allowed := image.NewFromIdentifier(strings.TrimSpace(imageName))
+ imageList = append(imageList, allowed)
+ }
+ appImages := ApplicationImages{}
+ appImages.Application = app
+ appImages.Images = imageList
+ appsForUpdate[app.GetName()] = appImages
+ }
+ }
+
+ return appsForUpdate, nil
+}
+
+// ListApplications returns a list of all application names that the API user
+// has access to.
+func (client *ArgoCD) ListApplications() ([]v1alpha1.Application, error) {
+ conn, appClient, err := client.Client.NewApplicationClient()
+ if err != nil {
+ return nil, err
+ }
+ defer conn.Close()
+
+ apps, err := appClient.List(context.TODO(), &application.ApplicationQuery{})
+ if err != nil {
+ return nil, err
+ }
+
+ return apps.Items, nil
+}
+
+// getHelmParamNamesFromAnnotation inspects the given annotations for whether
+// the annotations for specifying Helm parameter names are being set and
+// returns their values.
+func getHelmParamNamesFromAnnotation(annotations map[string]string, symbolicName string) (string, string) {
+ // Return default values without symbolic name given
+ if symbolicName == "" {
+ return "image.name", "image.tag"
+ }
+
+ var annotationName, helmParamName, helmParamVersion string
+
+ // Image spec is a full-qualified specifier, if we have it, we return early
+ annotationName = fmt.Sprintf(common.HelmParamImageSpecAnnotation, symbolicName)
+ if param, ok := annotations[annotationName]; ok {
+ log.Tracef("found annotation %s", annotationName)
+ return strings.TrimSpace(param), ""
+ }
+
+ annotationName = fmt.Sprintf(common.HelmParamImageNameAnnotation, symbolicName)
+ if param, ok := annotations[annotationName]; ok {
+ log.Tracef("found annotation %s", annotationName)
+ helmParamName = param
+ }
+
+ annotationName = fmt.Sprintf(common.HelmParamImageTagAnnotation, symbolicName)
+ if param, ok := annotations[annotationName]; ok {
+ log.Tracef("found annotation %s", annotationName)
+ helmParamVersion = param
+ }
+
+ return helmParamName, helmParamVersion
+}
+
+// Get a named helm parameter from a list of parameters
+func getHelmParam(params []v1alpha1.HelmParameter, name string) *v1alpha1.HelmParameter {
+ for _, param := range params {
+ if param.Name == name {
+ return &param
+ }
+ }
+ return nil
+}
+
+// mergeHelmParams merges a list of Helm parameters specified by merge into the
+// Helm parameters given as src.
+func mergeHelmParams(src []v1alpha1.HelmParameter, merge []v1alpha1.HelmParameter) []v1alpha1.HelmParameter {
+ retParams := make([]v1alpha1.HelmParameter, 0)
+ merged := make(map[string]interface{})
+
+ // first look for params that need replacement
+ for _, srcParam := range src {
+ found := false
+ for _, mergeParam := range merge {
+ if srcParam.Name == mergeParam.Name {
+ retParams = append(retParams, mergeParam)
+ merged[mergeParam.Name] = true
+ found = true
+ break
+ }
+ }
+ if !found {
+ retParams = append(retParams, srcParam)
+ }
+ }
+
+ // then check which we still need in dest list and merge those, too
+ for _, mergeParam := range merge {
+ if _, ok := merged[mergeParam.Name]; !ok {
+ retParams = append(retParams, mergeParam)
+ }
+ }
+
+ return retParams
+}
+
+// Set image parameters for a Helm application
+func (client *ArgoCD) SetHelmImage(appName string, newImage *image.ContainerImage) error {
+ conn, appClient, err := client.Client.NewApplicationClient()
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ app, err := appClient.Get(context.TODO(), &application.ApplicationQuery{Name: &appName})
+ if err != nil {
+ return err
+ }
+
+ helmParamImageName, helmParamImageTag := getHelmParamNamesFromAnnotation(app.GetAnnotations(), newImage.SymbolicName)
+ log.WithContext().
+ AddField("application", appName).
+ AddField("image", newImage.GetFullNameWithoutTag()).
+ Debugf("target parameters: image.name=%s, image.tag=%s", helmParamImageName, helmParamImageTag)
+
+ if appType := getApplicationType(app); appType != ApplicationTypeHelm {
+ return fmt.Errorf("cannot set Helm params on non-Helm application")
+ }
+
+ mergeParams := make([]v1alpha1.HelmParameter, 0)
+ if helmParamImageName != "" {
+ var p v1alpha1.HelmParameter
+ if helmParamImageTag == "" {
+ p = v1alpha1.HelmParameter{Name: helmParamImageName, Value: fmt.Sprintf("%s:%s", newImage.GetFullNameWithTag(), newImage.ImageTag)}
+ } else {
+ p = v1alpha1.HelmParameter{Name: helmParamImageName, Value: newImage.GetFullNameWithoutTag()}
+ }
+ mergeParams = append(mergeParams, p)
+ }
+
+ if helmParamImageTag != "" {
+ mergeParams = append(mergeParams, v1alpha1.HelmParameter{Name: helmParamImageTag, Value: newImage.ImageTag})
+ }
+
+ if app.Spec.Source.Helm == nil {
+ app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{}
+ }
+
+ if app.Spec.Source.Helm.Parameters == nil {
+ app.Spec.Source.Helm.Parameters = make([]v1alpha1.HelmParameter, 0)
+ }
+
+ app.Spec.Source.Helm.Parameters = mergeHelmParams(app.Spec.Source.Helm.Parameters, mergeParams)
+
+ _, err = appClient.UpdateSpec(context.TODO(), &application.ApplicationUpdateSpecRequest{Name: &appName, Spec: app.Spec})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Set a Kustomize image
+func (client *ArgoCD) SetKustomizeImage(appName string, newImage *image.ContainerImage) error {
+ conn, appClient, err := client.Client.NewApplicationClient()
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ app, err := appClient.Get(context.TODO(), &application.ApplicationQuery{Name: &appName})
+ if err != nil {
+ return err
+ }
+
+ if appType := getApplicationType(app); appType != ApplicationTypeKustomize {
+ return fmt.Errorf("cannot set Kustomize image on non-Kustomize application")
+ }
+
+ if app.Spec.Source.Kustomize == nil {
+ app.Spec.Source.Kustomize = &v1alpha1.ApplicationSourceKustomize{}
+ }
+
+ app.Spec.Source.Kustomize.MergeImage(v1alpha1.KustomizeImage(newImage.String()))
+
+ _, err = appClient.UpdateSpec(context.TODO(), &application.ApplicationUpdateSpecRequest{Name: &appName, Spec: app.Spec})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetImagesFromApplication returns the list of known images for the given application
+func (client *ArgoCD) GetImagesFromApplication(appName string) (image.ContainerImageList, error) {
+ images := make(image.ContainerImageList, 0)
+ conn, appClient, err := client.Client.NewApplicationClient()
+ if err != nil {
+ return nil, err
+ }
+ defer conn.Close()
+ app, err := appClient.Get(context.TODO(), &application.ApplicationQuery{Name: &appName})
+ if err != nil {
+ return nil, err
+ }
+
+ for _, imageStr := range app.Status.Summary.Images {
+ image := image.NewFromIdentifier(imageStr)
+ images = append(images, image)
+ }
+
+ return images, nil
+}
+
+// GetApplicationTypeByName first retrieves application with given appName and
+// returns its application type
+func (client *ArgoCD) GetApplicationTypeByName(appName string) (ApplicationType, error) {
+ conn, appClient, err := client.Client.NewApplicationClient()
+ if err != nil {
+ return ApplicationTypeUnsupported, err
+ }
+ defer conn.Close()
+
+ app, err := appClient.Get(context.TODO(), &application.ApplicationQuery{Name: &appName})
+ if err != nil {
+ return ApplicationTypeUnsupported, err
+ }
+ return getApplicationType(app), nil
+}
+
+// GetApplicationType returns the type of the ArgoCD application
+func (client *ArgoCD) GetApplicationType(app *v1alpha1.Application) ApplicationType {
+ return getApplicationType(app)
+}
+
+// IsValidApplicationType returns true if we can update the application
+func IsValidApplicationType(app *v1alpha1.Application) bool {
+ return getApplicationType(app) != ApplicationTypeUnsupported
+}
+
+// getApplicationType returns the type of the application
+func getApplicationType(app *v1alpha1.Application) ApplicationType {
+ if app.Status.SourceType == v1alpha1.ApplicationSourceTypeKustomize {
+ return ApplicationTypeKustomize
+ } else if app.Status.SourceType == v1alpha1.ApplicationSourceTypeHelm {
+ return ApplicationTypeHelm
+ } else {
+ return ApplicationTypeUnsupported
+ }
+}
+
+// String returns a string representation of the application type
+func (a ApplicationType) String() string {
+ switch a {
+ case ApplicationTypeKustomize:
+ return "Kustomize"
+ case ApplicationTypeHelm:
+ return "Helm"
+ case ApplicationTypeUnsupported:
+ return "Unsupported"
+ default:
+ return "Unknown"
+ }
+}
diff --git a/pkg/argocd/argocd_test.go b/pkg/argocd/argocd_test.go
new file mode 100644
index 0000000..e7d45f1
--- /dev/null
+++ b/pkg/argocd/argocd_test.go
@@ -0,0 +1,146 @@
+package argocd
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/common"
+
+ "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_GetHelmParamAnnotations(t *testing.T) {
+ t.Run("Get parameter names without symbolic names", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageSpecAnnotation, "myimg"): "image.blub",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "myimg"): "image.blab",
+ }
+ name, tag := getHelmParamNamesFromAnnotation(annotations, "")
+ assert.Equal(t, "image.name", name)
+ assert.Equal(t, "image.tag", tag)
+ })
+
+ t.Run("Find existing image spec annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageSpecAnnotation, "myimg"): "image.path",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "myimg"): "image.tag",
+ }
+ name, tag := getHelmParamNamesFromAnnotation(annotations, "myimg")
+ assert.Equal(t, "image.path", name)
+ assert.Empty(t, tag)
+ })
+
+ t.Run("Find existing image name and image tag annotations", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageNameAnnotation, "myimg"): "image.name",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "myimg"): "image.tag",
+ }
+ name, tag := getHelmParamNamesFromAnnotation(annotations, "myimg")
+ assert.Equal(t, "image.name", name)
+ assert.Equal(t, "image.tag", tag)
+ })
+
+ t.Run("Find non-existing image name and image tag annotations", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageNameAnnotation, "otherimg"): "image.name",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "otherimg"): "image.tag",
+ }
+ name, tag := getHelmParamNamesFromAnnotation(annotations, "myimg")
+ assert.Empty(t, name)
+ assert.Empty(t, tag)
+ })
+
+ t.Run("Find existing image tag annotations", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "myimg"): "image.tag",
+ }
+ name, tag := getHelmParamNamesFromAnnotation(annotations, "myimg")
+ assert.Empty(t, name)
+ assert.Equal(t, "image.tag", tag)
+ })
+
+ t.Run("No suitable annotations found", func(t *testing.T) {
+ annotations := map[string]string{}
+ name, tag := getHelmParamNamesFromAnnotation(annotations, "myimg")
+ assert.Empty(t, name)
+ assert.Empty(t, tag)
+ })
+
+}
+
+func Test_MergeHelmParams(t *testing.T) {
+ t.Run("Merge set with existing parameters", func(t *testing.T) {
+ srcParams := []v1alpha1.HelmParameter{
+ {
+ Name: "someparam",
+ Value: "somevalue",
+ },
+ {
+ Name: "image.name",
+ Value: "foobar",
+ },
+ {
+ Name: "otherparam",
+ Value: "othervalue",
+ },
+ {
+ Name: "image.tag",
+ Value: "1.2.3",
+ },
+ }
+ mergeParams := []v1alpha1.HelmParameter{
+ {
+ Name: "image.name",
+ Value: "foobar",
+ },
+ {
+ Name: "image.tag",
+ Value: "1.2.4",
+ },
+ }
+
+ dstParams := mergeHelmParams(srcParams, mergeParams)
+
+ param := getHelmParam(dstParams, "someparam")
+ require.NotNil(t, param)
+ assert.Equal(t, "somevalue", param.Value)
+
+ param = getHelmParam(dstParams, "otherparam")
+ require.NotNil(t, param)
+ assert.Equal(t, "othervalue", param.Value)
+
+ param = getHelmParam(dstParams, "image.name")
+ require.NotNil(t, param)
+ assert.Equal(t, "foobar", param.Value)
+
+ param = getHelmParam(dstParams, "image.tag")
+ require.NotNil(t, param)
+ assert.Equal(t, "1.2.4", param.Value)
+ })
+
+ t.Run("Merge set with empty src parameters", func(t *testing.T) {
+ srcParams := []v1alpha1.HelmParameter{}
+ mergeParams := []v1alpha1.HelmParameter{
+ {
+ Name: "image.name",
+ Value: "foobar",
+ },
+ {
+ Name: "image.tag",
+ Value: "1.2.4",
+ },
+ }
+
+ dstParams := mergeHelmParams(srcParams, mergeParams)
+
+ param := getHelmParam(dstParams, "image.name")
+ require.NotNil(t, param)
+ assert.Equal(t, "foobar", param.Value)
+
+ param = getHelmParam(dstParams, "image.tag")
+ require.NotNil(t, param)
+ assert.Equal(t, "1.2.4", param.Value)
+ })
+}
diff --git a/pkg/client/kubernetes.go b/pkg/client/kubernetes.go
new file mode 100644
index 0000000..88df55d
--- /dev/null
+++ b/pkg/client/kubernetes.go
@@ -0,0 +1,65 @@
+package client
+
+// Kubernetes client related code
+
+import (
+ "fmt"
+
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+type KubernetesClient struct {
+ Clientset kubernetes.Interface
+}
+
+// NewKubernetesClient creates a new Kubernetes client object from given
+// configuration file. If configuration file is the empty string, in-cluster
+// client will be created.
+func NewKubernetesClient(kubeconfig string) (*KubernetesClient, error) {
+ kClient := KubernetesClient{}
+
+ var config *rest.Config
+ var err error
+
+ if kubeconfig != "" {
+ config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
+ } else {
+ config, err = rest.InClusterConfig()
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ kClient.Clientset = clientset
+ return &kClient, nil
+}
+
+// GetSecretData returns the raw data from named K8s secret in given namespace
+func (client *KubernetesClient) GetSecretData(namespace string, secretName string) (map[string][]byte, error) {
+ secret, err := client.Clientset.CoreV1().Secrets(namespace).Get(secretName, v1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ return secret.Data, nil
+}
+
+// GetSecretField returns the value of a field from named K8s secret in given namespace
+func (client *KubernetesClient) GetSecretField(namespace string, secretName string, field string) (string, error) {
+ secret, err := client.GetSecretData(namespace, secretName)
+ if err != nil {
+ return "", err
+ }
+ if data, ok := secret[field]; !ok {
+ return "", fmt.Errorf("secret '%s/%s' does not have a field '%s'", namespace, secretName, field)
+ } else {
+ return string(data), nil
+ }
+}
diff --git a/pkg/client/kubernetes_test.go b/pkg/client/kubernetes_test.go
new file mode 100644
index 0000000..4629858
--- /dev/null
+++ b/pkg/client/kubernetes_test.go
@@ -0,0 +1,72 @@
+package client
+
+import (
+ "os"
+ "testing"
+
+ "github.com/argoproj-labs/argocd-image-updater/test/fake"
+ "github.com/argoproj-labs/argocd-image-updater/test/fixture"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_NewKubernetesClient(t *testing.T) {
+ t.Run("Get new K8s client for remote cluster instance", func(t *testing.T) {
+ client, err := NewKubernetesClient("../../test/testdata/kubernetes/config")
+ require.NoError(t, err)
+ assert.NotNil(t, client)
+ })
+
+ t.Run("Get new K8s client for in-cluster instance", func(t *testing.T) {
+ os.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1")
+ os.Setenv("KUBERNETES_SERVICE_PORT", "6443")
+ defer os.Setenv("KUBERNETES_SERVICE_HOST", "")
+ defer os.Setenv("KUBERNETES_SERVICE_PORT", "")
+ client, err := NewKubernetesClient("")
+ // We cannot create /var/run/secrets/kubernetes.io/serviceaccount/token so
+ // we just assume error and look for that path in error message.
+ assert.Errorf(t, err, "open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory")
+ assert.Nil(t, client)
+ })
+}
+
+func Test_GetDataFromSecrets(t *testing.T) {
+ t.Run("Get all data from dummy secret", func(t *testing.T) {
+ secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json")
+ clientset := fake.NewFakeClientsetWithResources(secret)
+ client := &KubernetesClient{Clientset: clientset}
+ data, err := client.GetSecretData("test-namespace", "test-secret")
+ require.NoError(t, err)
+ require.NotNil(t, data)
+ assert.Len(t, data, 1)
+ assert.Equal(t, "argocd", string(data["namespace"]))
+ })
+
+ t.Run("Get string data from dummy secret existing field", func(t *testing.T) {
+ secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json")
+ clientset := fake.NewFakeClientsetWithResources(secret)
+ client := &KubernetesClient{Clientset: clientset}
+ data, err := client.GetSecretField("test-namespace", "test-secret", "namespace")
+ require.NoError(t, err)
+ assert.Equal(t, "argocd", data)
+ })
+
+ t.Run("Get string data from dummy secret non-existing field", func(t *testing.T) {
+ secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json")
+ clientset := fake.NewFakeClientsetWithResources(secret)
+ client := &KubernetesClient{Clientset: clientset}
+ data, err := client.GetSecretField("test-namespace", "test-secret", "nonexisting")
+ require.Error(t, err)
+ require.Empty(t, data)
+ })
+
+ t.Run("Get string data from non-existing secret non-existing field", func(t *testing.T) {
+ secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json")
+ clientset := fake.NewFakeClientsetWithResources(secret)
+ client := &KubernetesClient{Clientset: clientset}
+ data, err := client.GetSecretField("test-namespace", "test", "namespace")
+ require.Error(t, err)
+ require.Empty(t, data)
+ })
+}
diff --git a/pkg/common/constants.go b/pkg/common/constants.go
new file mode 100644
index 0000000..9488be5
--- /dev/null
+++ b/pkg/common/constants.go
@@ -0,0 +1,14 @@
+package common
+
+// This file contains a list of constants required by other packages
+
+// The annotation on the application resources to indicate the list of images
+// allowed for updates.
+const ImageUpdaterAnnotation = "argocd-image-updater.argoproj.io/image-list"
+
+const HelmParamImageNameAnnotation = "argocd-image-update.argoproj.io/%s.image-name"
+const HelmParamImageTagAnnotation = "argocd-image-update.argoproj.io/%s.image-tag"
+const HelmParamImageSpecAnnotation = "argocd-image-update.argoproj.io/%s.image-spec"
+
+// gcr.io=secret:argocd/mysecret,docker.io=env:FOOBAR
+const SecretListAnnotation = "argocd-image-updater.argoproj.io/pullsecrets"
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 0000000..d912156
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1 @@
+package config
diff --git a/pkg/health/health.go b/pkg/health/health.go
new file mode 100644
index 0000000..457a310
--- /dev/null
+++ b/pkg/health/health.go
@@ -0,0 +1,24 @@
+package health
+
+// Most simple health check probe to see whether our server is still alive
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+)
+
+func StartHealthServer(port int) chan error {
+ errCh := make(chan error)
+ go func() {
+ http.HandleFunc("/healthz", HealthProbe)
+ errCh <- http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
+ }()
+ return errCh
+}
+
+func HealthProbe(w http.ResponseWriter, r *http.Request) {
+ log.Tracef("/healthz ping request received, replying with pong")
+ fmt.Fprintf(w, "OK\n")
+}
diff --git a/pkg/image/credentials.go b/pkg/image/credentials.go
new file mode 100644
index 0000000..99608f2
--- /dev/null
+++ b/pkg/image/credentials.go
@@ -0,0 +1,205 @@
+package image
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/client"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+)
+
+type CredentialSourceType int
+
+const (
+ CredentialSourceUnknown CredentialSourceType = 0
+ CredentialSourcePullSecret CredentialSourceType = 1
+ CredentialSourceSecret CredentialSourceType = 2
+ CredentialSourceEnv CredentialSourceType = 3
+)
+
+type CredentialSource struct {
+ Type CredentialSourceType
+ Registry string
+ SecretNamespace string
+ SecretName string
+ SecretField string
+ EnvName string
+}
+
+type Credential struct {
+ Username string
+ Password string
+}
+
+const pullSecretField = ".dockerconfigjson"
+
+// gcr.io=secret:foo/bar#baz
+// gcr.io=pullsecret:foo/bar
+// gcr.io=env:FOOBAR
+
+func ParseCredentialSource(credentialSource string, requirePrefix bool) (*CredentialSource, error) {
+ src := CredentialSource{}
+ var secretDef string
+ tokens := strings.SplitN(credentialSource, "=", 2)
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ if requirePrefix {
+ return nil, fmt.Errorf("invalid credential spec: %s", credentialSource)
+ }
+ secretDef = credentialSource
+ } else {
+ src.Registry = tokens[0]
+ secretDef = tokens[1]
+ }
+
+ tokens = strings.Split(secretDef, ":")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return nil, fmt.Errorf("invalid credential spec: %s", credentialSource)
+ }
+
+ var err error
+ switch strings.ToLower(tokens[0]) {
+ case "secret":
+ err = src.parseSecretDefinition(tokens[1])
+ src.Type = CredentialSourceSecret
+ case "pullsecret":
+ err = src.parsePullSecretDefinition(tokens[1])
+ src.Type = CredentialSourcePullSecret
+ case "env":
+ err = src.parseEnvDefinition(tokens[1])
+ src.Type = CredentialSourceEnv
+ default:
+ err = fmt.Errorf("unknown credential source: %s", tokens[0])
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &src, nil
+}
+
+// FetchCredentials fetches the credentials for a given registry according to
+// the credential source.
+func (src *CredentialSource) FetchCredentials(registryURL string, kubeclient *client.KubernetesClient) (*Credential, error) {
+ var creds Credential
+ switch src.Type {
+ case CredentialSourceEnv:
+ credEnv := os.Getenv(src.EnvName)
+ if credEnv == "" {
+ return nil, fmt.Errorf("could not fetch credentials: env '%s' is not set", src.EnvName)
+ }
+ tokens := strings.SplitN(credEnv, ":", 2)
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return nil, fmt.Errorf("could not fetch credentials: value of %s is malformed", src.EnvName)
+ }
+ creds.Username = tokens[0]
+ creds.Password = tokens[1]
+ return &creds, nil
+ case CredentialSourceSecret:
+ data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretNamespace, src.SecretName, src.SecretField, err)
+ }
+ tokens := strings.SplitN(data, ":", 2)
+ if len(tokens) != 2 {
+ return nil, fmt.Errorf("invalid credentials in secret %s/%s, field %s", src.SecretNamespace, src.SecretName, src.SecretField)
+ }
+ creds.Username = tokens[0]
+ creds.Password = tokens[1]
+ return &creds, nil
+ case CredentialSourcePullSecret:
+ src.SecretField = pullSecretField
+ data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretNamespace, src.SecretName, src.SecretField, err)
+ }
+ creds.Username, creds.Password, err = parseDockerConfigJson(registryURL, data)
+ if err != nil {
+ return nil, err
+ }
+ return &creds, nil
+ default:
+ return nil, fmt.Errorf("unknown credential type")
+ }
+}
+
+// Parse a secret definition in form of 'namespace/name#field'
+func (src *CredentialSource) parseSecretDefinition(definition string) error {
+ tokens := strings.Split(definition, "#")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return fmt.Errorf("invalid secret definition: %s", definition)
+ }
+ src.SecretField = tokens[1]
+ tokens = strings.Split(tokens[0], "/")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return fmt.Errorf("invalid secret definition: %s", definition)
+ }
+ src.SecretNamespace = tokens[0]
+ src.SecretName = tokens[1]
+
+ return nil
+}
+
+// Parse an image pull secret definition in form of 'namespace/name'
+func (src *CredentialSource) parsePullSecretDefinition(definition string) error {
+ tokens := strings.Split(definition, "/")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return fmt.Errorf("invalid secret definition: %s", definition)
+ }
+
+ src.SecretNamespace = tokens[0]
+ src.SecretName = tokens[1]
+ src.SecretField = pullSecretField
+
+ return nil
+}
+
+// Parse an environment definition
+func (src *CredentialSource) parseEnvDefinition(definition string) error {
+ src.EnvName = definition
+ return nil
+}
+
+// This unmarshals & parses Docker's config.json file, returning username and
+// password for given registry URL
+func parseDockerConfigJson(registryURL string, jsonSource string) (string, string, error) {
+ var dockerConf map[string]interface{}
+ err := json.Unmarshal([]byte(jsonSource), &dockerConf)
+ if err != nil {
+ return "", "", err
+ }
+ auths, ok := dockerConf["auths"].(map[string]interface{})
+ if !ok {
+ return "", "", fmt.Errorf("no credentials in image pull secret")
+ }
+
+ for registry, authConf := range auths {
+ if !strings.HasPrefix(registry, registryURL) {
+ log.Tracef("found registry %s in image pull secret, but we want %s - skipping", registry, registryURL)
+ continue
+ }
+ authEntry, ok := authConf.(map[string]interface{})
+ if !ok {
+ return "", "", fmt.Errorf("invalid auth entry for registry entry %s ('auths' entry should be map)", registry)
+ }
+ authString, ok := authEntry["auth"].(string)
+ if !ok {
+ return "", "", fmt.Errorf("invalid auth token for registry entry %s ('auth' should be string')", registry)
+ }
+ authToken, err := base64.StdEncoding.DecodeString(authString)
+ if err != nil {
+ return "", "", fmt.Errorf("could not base64-decode auth data for registry entry %s: %v", registry, err)
+ }
+ tokens := strings.SplitN(string(authToken), ":", 2)
+ if len(tokens) != 2 {
+ return "", "", fmt.Errorf("invalid data after base64 decoding auth entry for registry entry %s", registry)
+ }
+
+ return tokens[0], tokens[1], nil
+ }
+
+ return "", "", fmt.Errorf("no valid auth entry for registry %s found in image pull secret", registryURL)
+}
diff --git a/pkg/image/credentials_test.go b/pkg/image/credentials_test.go
new file mode 100644
index 0000000..9dc6dd9
--- /dev/null
+++ b/pkg/image/credentials_test.go
@@ -0,0 +1,198 @@
+package image
+
+import (
+ "os"
+ "testing"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/client"
+ "github.com/argoproj-labs/argocd-image-updater/test/fake"
+ "github.com/argoproj-labs/argocd-image-updater/test/fixture"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_ParseCredentialAnnotation(t *testing.T) {
+ t.Run("Parse valid credentials definition of type secret", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:mynamespace/mysecret#anyfield", true)
+ assert.NoError(t, err)
+ assert.Equal(t, "gcr.io", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, "anyfield", src.SecretField)
+ })
+
+ t.Run("Parse valid credentials definition of type pullsecret", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace/mysecret", true)
+ assert.NoError(t, err)
+ assert.Equal(t, "gcr.io", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, ".dockerconfigjson", src.SecretField)
+ })
+
+ t.Run("Parse invalid secret definition - missing registry", func(t *testing.T) {
+ src, err := ParseCredentialSource("secret:mynamespace/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - empty registry", func(t *testing.T) {
+ src, err := ParseCredentialSource("=secret:mynamespace/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - unknown credential type", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secrets:mynamespace/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - missing field", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:mynamespace/mysecret#", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - missing namespace", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid credential definition - missing name", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:mynamespace/#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid credential definition - missing most", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid pullsecret definition - missing namespace", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:/mysecret", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid credential definition - missing name", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+}
+
+func Test_ParseCredentialReference(t *testing.T) {
+ t.Run("Parse valid credentials definition of type secret", func(t *testing.T) {
+ src, err := ParseCredentialSource("secret:mynamespace/mysecret#anyfield", false)
+ assert.NoError(t, err)
+ assert.Equal(t, "", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, "anyfield", src.SecretField)
+ })
+
+ t.Run("Parse valid credentials definition of type pullsecret", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace/mysecret", false)
+ assert.NoError(t, err)
+ assert.Equal(t, "gcr.io", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, ".dockerconfigjson", src.SecretField)
+ })
+
+ t.Run("Parse invalid secret definition - empty registry", func(t *testing.T) {
+ src, err := ParseCredentialSource("=secret:mynamespace/mysecret#anyfield", false)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+}
+
+func Test_FetchCredentialsFromPullSecret(t *testing.T) {
+ t.Run("Fetch credentials from pull secret", func(t *testing.T) {
+ dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ secretData := make(map[string][]byte)
+ secretData[pullSecretField] = []byte(dockerJson)
+ pullSecret := fixture.NewSecret("test", "test", secretData)
+ clientset := fake.NewFakeClientsetWithResources(pullSecret)
+ credSrc := &CredentialSource{
+ Type: CredentialSourcePullSecret,
+ Registry: "https://registry-1.docker.io/v2",
+ SecretNamespace: "test",
+ SecretName: "test",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &client.KubernetesClient{Clientset: clientset})
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "foo", creds.Username)
+ assert.Equal(t, "bar", creds.Password)
+ })
+}
+
+func Test_FetchCredentialsFromEnv(t *testing.T) {
+ t.Run("Fetch credentials from environment", func(t *testing.T) {
+ err := os.Setenv("MY_SECRET_ENV", "foo:bar")
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceEnv,
+ Registry: "https://registry-1.docker.io/v2",
+ EnvName: "MY_SECRET_ENV",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "foo", creds.Username)
+ assert.Equal(t, "bar", creds.Password)
+ })
+
+ t.Run("Fetch credentials from environment with missing env var", func(t *testing.T) {
+ err := os.Setenv("MY_SECRET_ENV", "")
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceEnv,
+ Registry: "https://registry-1.docker.io/v2",
+ EnvName: "MY_SECRET_ENV",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Error(t, err)
+ require.Nil(t, creds)
+ })
+
+ t.Run("Fetch credentials from environment with invalid value in env var", func(t *testing.T) {
+ for _, value := range []string{"babayaga", "foo:", "bar:", ":"} {
+ err := os.Setenv("MY_SECRET_ENV", value)
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceEnv,
+ Registry: "https://registry-1.docker.io/v2",
+ EnvName: "MY_SECRET_ENV",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Error(t, err)
+ require.Nil(t, creds)
+ }
+ })
+}
+
+func Test_ParseDockerConfig(t *testing.T) {
+ t.Run("Parse valid Docker configuration with matching registry", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ username, password, err := parseDockerConfigJson("https://registry-1.docker.io", config)
+ require.NoError(t, err)
+ assert.Equal(t, "foo", username)
+ assert.Equal(t, "bar", password)
+ })
+
+ t.Run("Parse valid Docker configuration without matching registry", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ username, password, err := parseDockerConfigJson("https://gcr.io", config)
+ assert.Error(t, err)
+ assert.Empty(t, username)
+ assert.Empty(t, password)
+ })
+}
diff --git a/pkg/image/image.go b/pkg/image/image.go
new file mode 100644
index 0000000..22e5d80
--- /dev/null
+++ b/pkg/image/image.go
@@ -0,0 +1,155 @@
+package image
+
+import (
+ "strings"
+)
+
+type ContainerImage struct {
+ RegistryURL string
+ ImageName string
+ ImageTag string
+ SymbolicName string
+ HelmParamImageName string
+ HelmParamImageVersion string
+ original string
+}
+
+type ContainerImageList []*ContainerImage
+
+// NewFromIdentifier parses an image identifier and returns a populated ContainerImage
+func NewFromIdentifier(identifier string) *ContainerImage {
+ img := ContainerImage{}
+ img.RegistryURL = getRegistryFromIdentifier(identifier)
+ img.SymbolicName, img.ImageName, img.ImageTag = getImageTagFromIdentifier(identifier)
+ img.original = identifier
+ return &img
+}
+
+// String returns the string representation of given ContainerImage
+func (img *ContainerImage) String() string {
+ str := ""
+ if img.SymbolicName != "" {
+ str += img.SymbolicName
+ str += "="
+ }
+ if img.RegistryURL != "" {
+ str += img.RegistryURL + "/"
+ }
+ str += img.ImageName
+ if img.ImageTag != "" {
+ str += ":"
+ str += img.ImageTag
+ }
+ return str
+}
+
+func (img *ContainerImage) GetFullNameWithoutTag() string {
+ str := ""
+ if img.RegistryURL != "" {
+ str += img.RegistryURL + "/"
+ }
+ str += img.ImageName
+ return str
+}
+
+func (img *ContainerImage) GetFullNameWithTag() string {
+ str := ""
+ if img.RegistryURL != "" {
+ str += img.RegistryURL + "/"
+ }
+ str += img.ImageName
+ if img.ImageTag != "" {
+ str += ":"
+ str += img.ImageTag
+ }
+ return str
+}
+
+func (img *ContainerImage) Original() string {
+ return img.original
+}
+
+// IsUpdatable checks whether the given image can be updated with newTag while
+// taking tagSpec into account. tagSpec must be given as a semver compatible
+// version spec, i.e. ^1.0 or ~2.1
+func (img *ContainerImage) IsUpdatable(newTag, tagSpec string) bool {
+ return false
+}
+
+// WithTag returns a copy of img with new tag information set
+func (img *ContainerImage) WithTag(newTag string) *ContainerImage {
+ nimg := &ContainerImage{}
+ nimg.RegistryURL = img.RegistryURL
+ nimg.ImageName = img.ImageName
+ nimg.ImageTag = newTag
+ nimg.SymbolicName = img.SymbolicName
+ nimg.HelmParamImageName = img.HelmParamImageName
+ nimg.HelmParamImageVersion = img.HelmParamImageVersion
+ return nimg
+}
+
+// ContainsImage checks whether img is contained in a list of images
+func (list *ContainerImageList) ContainsImage(img *ContainerImage, checkVersion bool) *ContainerImage {
+ for _, image := range *list {
+ if img.ImageName == image.ImageName && image.RegistryURL == img.RegistryURL {
+ if !checkVersion || image.ImageTag == img.ImageTag {
+ return image
+ }
+ }
+ }
+ return nil
+}
+
+// String Returns the name of all images as a string, seperated using comma
+func (list *ContainerImageList) String() string {
+ imgNameList := make([]string, 0)
+ for _, image := range *list {
+ imgNameList = append(imgNameList, image.String())
+ }
+ return strings.Join(imgNameList, ",")
+}
+
+// Gets the registry URL from an image identifier
+func getRegistryFromIdentifier(identifier string) string {
+ var imageString string
+ comp := strings.Split(identifier, "=")
+ if len(comp) > 1 {
+ imageString = comp[1]
+ } else {
+ imageString = identifier
+ }
+ comp = strings.Split(imageString, "/")
+ if len(comp) > 1 && strings.Contains(comp[0], ".") {
+ return comp[0]
+ } else {
+ return ""
+ }
+}
+
+// Gets the image name and tag from an image identifier
+func getImageTagFromIdentifier(identifier string) (string, string, string) {
+ var imageString string
+ var sourceName string
+
+ // The original name is prepended to the image name, seperated by =
+ comp := strings.Split(identifier, "=")
+ if len(comp) == 2 {
+ sourceName = comp[0]
+ imageString = comp[1]
+ } else {
+ imageString = identifier
+ }
+
+ // Strip any repository identifier from the string
+ comp = strings.Split(imageString, "/")
+ if len(comp) > 1 && strings.Contains(comp[0], ".") {
+ imageString = strings.Join(comp[1:], "/")
+ }
+
+ comp = strings.Split(imageString, ":")
+ if len(comp) != 2 {
+ return sourceName, imageString, ""
+ } else {
+ return sourceName, comp[0], comp[1]
+ }
+}
diff --git a/pkg/image/image_test.go b/pkg/image/image_test.go
new file mode 100644
index 0000000..875b66a
--- /dev/null
+++ b/pkg/image/image_test.go
@@ -0,0 +1,88 @@
+package image
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_ParseImageTags(t *testing.T) {
+ t.Run("Parse valid image name without registry info", func(t *testing.T) {
+ image := NewFromIdentifier("jannfis/test-image:0.1")
+ assert.Empty(t, image.RegistryURL)
+ assert.Empty(t, image.SymbolicName)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ assert.Equal(t, "0.1", image.ImageTag)
+ })
+
+ t.Run("Parse valid image name with registry info", func(t *testing.T) {
+ image := NewFromIdentifier("gcr.io/jannfis/test-image:0.1")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Empty(t, image.SymbolicName)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ assert.Equal(t, "0.1", image.ImageTag)
+ })
+
+ t.Run("Parse valid image name with source name and registry info", func(t *testing.T) {
+ image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image:0.1")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Equal(t, "jannfis/orig-image", image.SymbolicName)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ assert.Equal(t, "0.1", image.ImageTag)
+ })
+
+ t.Run("Parse image without version source name and registry info", func(t *testing.T) {
+ image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Equal(t, "jannfis/orig-image", image.SymbolicName)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ assert.Empty(t, image.ImageTag)
+ })
+}
+
+func Test_ImageToString(t *testing.T) {
+ t.Run("Get string representation of full-qualified image name", func(t *testing.T) {
+ imageName := "jannfis/argocd=jannfis/orig-image:0.1"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.String())
+ })
+ t.Run("Get string representation of full-qualified image name with registry", func(t *testing.T) {
+ imageName := "jannfis/argocd=gcr.io/jannfis/orig-image:0.1"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.String())
+ })
+ t.Run("Get string representation of full-qualified image name with registry", func(t *testing.T) {
+ imageName := "jannfis/argocd=gcr.io/jannfis/orig-image"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.String())
+ })
+ t.Run("Get original value", func(t *testing.T) {
+ imageName := "invalid==foo"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.Original())
+ })
+}
+
+func Test_WithTag(t *testing.T) {
+ t.Run("Get string representation of full-qualified image name", func(t *testing.T) {
+ imageName := "jannfis/argocd=jannfis/orig-image:0.1"
+ nimageName := "jannfis/argocd=jannfis/orig-image:0.2"
+ oImg := NewFromIdentifier(imageName)
+ nImg := oImg.WithTag("0.2")
+ assert.Equal(t, nimageName, nImg.String())
+ })
+}
+
+func Test_ContainerList(t *testing.T) {
+ t.Run("Test whether image is contained in list", func(t *testing.T) {
+ images := make(ContainerImageList, 0)
+ image_names := []string{"a/a:0.1", "a/b:1.2", "x/y=foo.bar/a/c:0.23"}
+ for _, n := range image_names {
+ images = append(images, NewFromIdentifier(n))
+ }
+ assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[0]), false))
+ assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[1]), false))
+ assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[2]), false))
+ assert.Nil(t, images.ContainsImage(NewFromIdentifier("foo/bar"), false))
+ })
+}
diff --git a/pkg/image/kustomize.go b/pkg/image/kustomize.go
new file mode 100644
index 0000000..ef7c88b
--- /dev/null
+++ b/pkg/image/kustomize.go
@@ -0,0 +1,39 @@
+package image
+
+import (
+ "strings"
+)
+
+// Shamelessly ripped from ArgoCD CLI code
+
+type KustomizeImage string
+
+func (i KustomizeImage) delim() string {
+ for _, d := range []string{"=", ":", "@"} {
+ if strings.Contains(string(i), d) {
+ return d
+ }
+ }
+ return ":"
+}
+
+// if the image name matches (i.e. up to the first delimiter)
+func (i KustomizeImage) Match(j KustomizeImage) bool {
+ delim := j.delim()
+ if !strings.Contains(string(j), delim) {
+ return false
+ }
+ return strings.HasPrefix(string(i), strings.Split(string(j), delim)[0])
+}
+
+type KustomizeImages []KustomizeImage
+
+// find the image or -1
+func (images KustomizeImages) Find(image KustomizeImage) int {
+ for i, a := range images {
+ if a.Match(image) {
+ return i
+ }
+ }
+ return -1
+}
diff --git a/pkg/image/version.go b/pkg/image/version.go
new file mode 100644
index 0000000..5dcafdd
--- /dev/null
+++ b/pkg/image/version.go
@@ -0,0 +1,71 @@
+package image
+
+import (
+ "sort"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+
+ "github.com/Masterminds/semver"
+)
+
+// GetNewestVersionFromTags returns the latest available version from a list of
+// tags while optionally taking a semver constraint into account. Returns the
+// original version if no new version could be found from the list of tags.
+func (img *ContainerImage) GetNewestVersionFromTags(constraint string, availableTags []string) (string, error) {
+ logCtx := log.NewContext()
+ logCtx.AddField("image", img.String())
+
+ // It makes no sense to proceed if we have no available tags
+ if len(availableTags) == 0 {
+ return img.ImageTag, nil
+ }
+
+ _, err := semver.NewVersion(img.ImageTag)
+ if err != nil {
+ return "", err
+ }
+
+ // The given constraint MUST match a semver constraint
+ var semverConstraint *semver.Constraints
+ if constraint != "" {
+ semverConstraint, err = semver.NewConstraint(constraint)
+ if err != nil {
+ logCtx.Errorf("invalid constraint '%s' given: '%v'", constraint, err)
+ return "", err
+ }
+ }
+
+ tagVersions := make([]*semver.Version, 0)
+
+ // Loop through all tags to check whether it's an update candidate.
+ for _, tag := range availableTags {
+
+ // Non-parseable tag does not mean error - just skip it
+ ver, err := semver.NewVersion(tag)
+ if err != nil {
+ continue
+ }
+
+ // If we have a version constraint, check image tag against it. If the
+ // constraint is not satisfied, skip tag.
+ if semverConstraint != nil {
+ if !semverConstraint.Check(ver) {
+ continue
+ }
+ }
+
+ // Append tag as update candidate
+ tagVersions = append(tagVersions, ver)
+ }
+
+ logCtx.Debugf("found %d from %d tags eligible for consideration", len(tagVersions), len(availableTags))
+
+ // Sort update candidates and return the most recent version in its original
+ // form, so we can later fetch it from the registry.
+ if len(tagVersions) > 0 {
+ sort.Sort(semver.Collection(tagVersions))
+ return tagVersions[len(tagVersions)-1].Original(), nil
+ } else {
+ return img.ImageTag, nil
+ }
+}
diff --git a/pkg/image/version_test.go b/pkg/image/version_test.go
new file mode 100644
index 0000000..6879106
--- /dev/null
+++ b/pkg/image/version_test.go
@@ -0,0 +1,59 @@
+package image
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_LatestVersion(t *testing.T) {
+ t.Run("Find the latest version without any constraint", func(t *testing.T) {
+ tagList := []string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"}
+ img := NewFromIdentifier("jannfis/test:1.0")
+ newTag, err := img.GetNewestVersionFromTags("", tagList)
+ require.NoError(t, err)
+ assert.Equal(t, "2.0.3", newTag)
+ })
+
+ t.Run("Find the latest version with a semver constraint on major", func(t *testing.T) {
+ tagList := []string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"}
+ img := NewFromIdentifier("jannfis/test:1.0")
+ newTag, err := img.GetNewestVersionFromTags("^1.0", tagList)
+ require.NoError(t, err)
+ assert.Equal(t, "1.1.2", newTag)
+ })
+
+ t.Run("Find the latest version with a semver constraint on patch", func(t *testing.T) {
+ tagList := []string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"}
+ img := NewFromIdentifier("jannfis/test:1.0")
+ newTag, err := img.GetNewestVersionFromTags("~1.0", tagList)
+ require.NoError(t, err)
+ assert.Equal(t, "1.0.1", newTag)
+ })
+
+ t.Run("Find the latest version with a semver constraint that has no match", func(t *testing.T) {
+ tagList := []string{"0.1", "0.5.1", "0.9", "2.0.3"}
+ img := NewFromIdentifier("jannfis/test:1.0")
+ newTag, err := img.GetNewestVersionFromTags("~1.0", tagList)
+ require.NoError(t, err)
+ assert.Equal(t, "1.0", newTag)
+ })
+
+ t.Run("Find the latest version with a semver constraint that is invalid", func(t *testing.T) {
+ tagList := []string{"0.1", "0.5.1", "0.9", "2.0.3"}
+ img := NewFromIdentifier("jannfis/test:1.0")
+ newTag, err := img.GetNewestVersionFromTags("latest", tagList)
+ assert.Error(t, err)
+ assert.Equal(t, "", newTag)
+ })
+
+ t.Run("Find the latest version with no tags", func(t *testing.T) {
+ tagList := []string{}
+ img := NewFromIdentifier("jannfis/test:1.0")
+ newTag, err := img.GetNewestVersionFromTags("~1.0", tagList)
+ require.NoError(t, err)
+ assert.Equal(t, "1.0", newTag)
+ })
+
+}
diff --git a/pkg/log/log.go b/pkg/log/log.go
new file mode 100644
index 0000000..83ab043
--- /dev/null
+++ b/pkg/log/log.go
@@ -0,0 +1,179 @@
+package log
+
+// Wrapper package around logrus whose main purpose is to support having
+// different output streams for error and non-error messages.
+//
+// Does not wrap every method of logrus package. If you need direct access,
+// use log.Log() to get the actuall logrus logger object.
+//
+// It might seem redundant, but we really want the different output streams.
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+)
+
+// Internal Logger object
+var logger *logrus.Logger
+
+// LogContext contains a structured context for logging
+type LogContext struct {
+ fields logrus.Fields
+ normalOut io.Writer
+ errorOut io.Writer
+}
+
+// NewContext returns a LogContext with default settings
+func NewContext() *LogContext {
+ var logctx LogContext
+ logctx.fields = make(logrus.Fields)
+ logctx.normalOut = os.Stdout
+ logctx.errorOut = os.Stderr
+ return &logctx
+}
+
+// SetLogLevel sets the log level to use for the logger
+func SetLogLevel(logLevel string) error {
+ switch strings.ToLower(logLevel) {
+ case "trace":
+ logger.SetLevel(logrus.TraceLevel)
+ case "debug":
+ logger.SetLevel(logrus.DebugLevel)
+ case "info":
+ logger.SetLevel(logrus.InfoLevel)
+ case "warn":
+ logger.SetLevel(logrus.WarnLevel)
+ case "error":
+ logger.SetLevel(logrus.ErrorLevel)
+ default:
+ return fmt.Errorf("invalid loglevel: %s", logLevel)
+ }
+ return nil
+}
+
+// WithContext is an alias for NewContext
+func WithContext() *LogContext {
+ return NewContext()
+}
+
+// AddField adds a structured field to logctx
+func (logctx *LogContext) AddField(key string, value interface{}) *LogContext {
+ logctx.fields[key] = value
+ return logctx
+}
+
+// Logger retrieves the native logger interface. Use with care.
+func Log() *logrus.Logger {
+ return logger
+}
+
+// Tracef logs a debug message for logctx to stdout
+func (logctx *LogContext) Tracef(format string, args ...interface{}) {
+ logger.SetOutput(logctx.normalOut)
+ if logctx.fields != nil && len(logctx.fields) > 0 {
+ logger.WithFields(logctx.fields).Tracef(format, args...)
+ } else {
+ logger.Tracef(format, args...)
+ }
+}
+
+// Debugf logs a debug message for logctx to stdout
+func (logctx *LogContext) Debugf(format string, args ...interface{}) {
+ logger.SetOutput(logctx.normalOut)
+ if logctx.fields != nil && len(logctx.fields) > 0 {
+ logger.WithFields(logctx.fields).Debugf(format, args...)
+ } else {
+ logger.Debugf(format, args...)
+ }
+}
+
+// Infof logs an informational message for logctx to stdout
+func (logctx *LogContext) Infof(format string, args ...interface{}) {
+ logger.SetOutput(logctx.normalOut)
+ if logctx.fields != nil && len(logctx.fields) > 0 {
+ logger.WithFields(logctx.fields).Infof(format, args...)
+ } else {
+ logger.Infof(format, args...)
+ }
+}
+
+// Warnf logs a warning message for logctx to stdout
+func (logctx *LogContext) Warnf(format string, args ...interface{}) {
+ logger.SetOutput(logctx.normalOut)
+ if logctx.fields != nil && len(logctx.fields) > 0 {
+ logger.WithFields(logctx.fields).Warnf(format, args...)
+ } else {
+ logger.Warnf(format, args...)
+ }
+}
+
+// Errorf logs a non-fatal error message for logctx to stdout
+func (logctx *LogContext) Errorf(format string, args ...interface{}) {
+ logger.SetOutput(logctx.errorOut)
+ if logctx.fields != nil && len(logctx.fields) > 0 {
+ logger.WithFields(logctx.fields).Errorf(format, args...)
+ } else {
+ logger.Errorf(format, args...)
+ }
+}
+
+// Fatalf logs a fatal error message for logctx to stdout
+func (logctx *LogContext) Fatalf(format string, args ...interface{}) {
+ logger.SetOutput(logctx.errorOut)
+ if logctx.fields != nil && len(logctx.fields) > 0 {
+ logger.WithFields(logctx.fields).Fatalf(format, args...)
+ } else {
+ logger.Fatalf(format, args...)
+ }
+}
+
+// Debugf logs a warning message without context to stdout
+func Tracef(format string, args ...interface{}) {
+ logCtx := NewContext()
+ logCtx.Tracef(format, args...)
+}
+
+// Debugf logs a warning message without context to stdout
+func Debugf(format string, args ...interface{}) {
+ logCtx := NewContext()
+ logCtx.Debugf(format, args...)
+}
+
+// Infof logs a warning message without context to stdout
+func Infof(format string, args ...interface{}) {
+ logCtx := NewContext()
+ logCtx.Infof(format, args...)
+}
+
+// Warnf logs a warning message without context to stdout
+func Warnf(format string, args ...interface{}) {
+ logCtx := NewContext()
+ logCtx.Warnf(format, args...)
+}
+
+// Errorf logs an error message without context to stderr
+func Errorf(format string, args ...interface{}) {
+ logCtx := NewContext()
+ logCtx.Errorf(format, args...)
+}
+
+// Fatalf logs a non-recoverable error message without context to stderr
+func Fatalf(format string, args ...interface{}) {
+ logCtx := NewContext()
+ logCtx.Fatalf(format, args...)
+}
+
+func disableLogColors() bool {
+ return strings.ToLower(os.Getenv("ENABLE_LOG_COLORS")) == "false"
+}
+
+// Initializes the logging subsystem with default values
+func init() {
+ logger = logrus.New()
+ logger.SetFormatter(&logrus.TextFormatter{DisableColors: disableLogColors()})
+ logger.SetLevel(logrus.DebugLevel)
+}
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
new file mode 100644
index 0000000..57e060c
--- /dev/null
+++ b/pkg/log/log_test.go
@@ -0,0 +1,155 @@
+package log
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/argoproj-labs/argocd-image-updater/test/fixture"
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_LogToStdout(t *testing.T) {
+ // We need tracing level
+ Log().SetLevel(logrus.TraceLevel)
+
+ t.Run("Test for Tracef() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ Tracef("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "this is a test")
+ assert.Contains(t, out, "level=trace")
+ })
+ t.Run("Test for Debugf() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ Debugf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "this is a test")
+ assert.Contains(t, out, "level=debug")
+ })
+ t.Run("Test for Infof() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ Infof("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "this is a test")
+ assert.Contains(t, out, "level=info")
+ })
+ t.Run("Test for Warnf() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ Warnf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "this is a test")
+ assert.Contains(t, out, "level=warn")
+ })
+ t.Run("Test for Errorf() to not log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ Errorf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Empty(t, out)
+ })
+}
+
+func Test_LogToStderr(t *testing.T) {
+ // We need tracing level
+ Log().SetLevel(logrus.TraceLevel)
+
+ t.Run("Test for Tracef() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStderr(func() {
+ Tracef("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Empty(t, out)
+ })
+ t.Run("Test for Debugf() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStderr(func() {
+ Debugf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Empty(t, out)
+ })
+ t.Run("Test for Infof() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStderr(func() {
+ Infof("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Empty(t, out)
+ })
+ t.Run("Test for Warnf() to log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStderr(func() {
+ Warnf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Empty(t, out)
+ })
+ t.Run("Test for Errorf() to not log to stdout", func(t *testing.T) {
+ out, err := fixture.CaptureStderr(func() {
+ Errorf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "this is a test")
+ assert.Contains(t, out, "level=error")
+ })
+}
+
+func Test_LoggerFields(t *testing.T) {
+ Log().SetLevel(logrus.TraceLevel)
+ t.Run("Test for Tracef() to log correctly with fields", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ WithContext().AddField("foo", "bar").Tracef("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "foo=bar")
+ assert.Contains(t, out, "msg=\"this is a test\"")
+ })
+ t.Run("Test for Debugf() to log correctly with fields", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ WithContext().AddField("foo", "bar").Debugf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "foo=bar")
+ assert.Contains(t, out, "msg=\"this is a test\"")
+ })
+ t.Run("Test for Infof() to log correctly with fields", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ WithContext().AddField("foo", "bar").Infof("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "foo=bar")
+ assert.Contains(t, out, "msg=\"this is a test\"")
+ })
+ t.Run("Test for Warnf() to log correctly with fields", func(t *testing.T) {
+ out, err := fixture.CaptureStdout(func() {
+ WithContext().AddField("foo", "bar").Warnf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "foo=bar")
+ assert.Contains(t, out, "msg=\"this is a test\"")
+ })
+ t.Run("Test for Errorf() to log correctly with fields", func(t *testing.T) {
+ out, err := fixture.CaptureStderr(func() {
+ WithContext().AddField("foo", "bar").Errorf("this is a test")
+ })
+ require.NoError(t, err)
+ assert.Contains(t, out, "foo=bar")
+ assert.Contains(t, out, "msg=\"this is a test\"")
+ })
+}
+
+func Test_LogLevel(t *testing.T) {
+ for _, level := range []string{"trace", "debug", "info", "warn", "error"} {
+ t.Run(fmt.Sprintf("Test set loglevel %s", level), func(t *testing.T) {
+ err := SetLogLevel(level)
+ assert.NoError(t, err)
+ })
+ }
+ t.Run("Test set invalid loglevel", func(t *testing.T) {
+ err := SetLogLevel("invalid")
+ assert.Error(t, err)
+ })
+}
diff --git a/pkg/registry/config.go b/pkg/registry/config.go
new file mode 100644
index 0000000..85aec51
--- /dev/null
+++ b/pkg/registry/config.go
@@ -0,0 +1,64 @@
+package registry
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+
+ "gopkg.in/yaml.v2"
+)
+
+type RegistryConfiguration struct {
+ Name string `yaml:"name"`
+ ApiURL string `yaml:"api_url"`
+ Ping bool `yaml:"ping,omitempty"`
+ Credentials string `yaml:"credentials,omitempty"`
+ Prefix string `yaml:"prefix,omitempty"`
+}
+
+type RegistryList struct {
+ Items []RegistryConfiguration `yaml:"registries"`
+}
+
+func LoadRegistryConfiguration(path string) error {
+ registryBytes, err := ioutil.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ registryList, err := ParseRegistryConfiguration(string(registryBytes))
+ if err != nil {
+ return err
+ }
+ for _, reg := range registryList.Items {
+ err = AddRegistryEndpoint(reg.Prefix, reg.Name, reg.ApiURL, "", "", reg.Credentials)
+ if err != nil {
+ return err
+ }
+ }
+ log.Infof("Loaded %d registry configurations from %s", len(registryList.Items), path)
+ return nil
+}
+
+// Parses a registry configuration from a YAML input string and returns a list
+// of registries.
+func ParseRegistryConfiguration(yamlSource string) (RegistryList, error) {
+ var regList RegistryList
+ err := yaml.UnmarshalStrict([]byte(yamlSource), &regList)
+ if err != nil {
+ return RegistryList{}, err
+ }
+
+ // validate the parsed list
+ for _, registry := range regList.Items {
+ if registry.Name == "" {
+ err = fmt.Errorf("registry name is missing for entry %v", registry)
+ }
+ }
+
+ if err != nil {
+ return RegistryList{}, err
+ }
+
+ return regList, nil
+}
diff --git a/pkg/registry/config_test.go b/pkg/registry/config_test.go
new file mode 100644
index 0000000..e44b166
--- /dev/null
+++ b/pkg/registry/config_test.go
@@ -0,0 +1,19 @@
+package registry
+
+import (
+ "testing"
+
+ "github.com/argoproj-labs/argocd-image-updater/test/fixture"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_ParseRegistryConfFromYaml(t *testing.T) {
+ t.Run("Parse from valid YAML", func(t *testing.T) {
+ data := fixture.MustReadFile("../../config/example-config.yaml")
+ regList, err := ParseRegistryConfiguration(data)
+ require.NoError(t, err)
+ assert.Len(t, regList.Items, 3)
+ })
+}
diff --git a/pkg/registry/endpoints.go b/pkg/registry/endpoints.go
new file mode 100644
index 0000000..d69fbdb
--- /dev/null
+++ b/pkg/registry/endpoints.go
@@ -0,0 +1,85 @@
+package registry
+
+import (
+ "fmt"
+ "sync"
+)
+
+// RegistryEndpoint holds information on how to access any specific registry API
+// endpoint.
+type RegistryEndpoint struct {
+ RegistryName string
+ RegistryPrefix string
+ RegistryAPI string
+ Username string
+ Password string
+ Ping bool
+ Credentials string
+
+ lock sync.RWMutex
+}
+
+// Map of configured registries, pre-filled with some well-known registries
+var registries map[string]*RegistryEndpoint = map[string]*RegistryEndpoint{
+ "": {
+ RegistryName: "Docker Hub",
+ RegistryPrefix: "",
+ RegistryAPI: "https://registry-1.docker.io",
+ Ping: true,
+ },
+ "gcr.io": {
+ RegistryName: "Google Container Registry",
+ RegistryPrefix: "gcr.io",
+ RegistryAPI: "https://gcr.io",
+ Ping: false,
+ },
+ "quay.io": {
+ RegistryName: "RedHat Quay",
+ RegistryPrefix: "quay.io",
+ RegistryAPI: "https://quay.io",
+ Ping: false,
+ },
+}
+
+// Simple RW mutex for concurrent access to registries map
+var registryLock sync.RWMutex
+
+// AddRegistryEndpoint adds registry endpoint information with the given details
+func AddRegistryEndpoint(prefix, name, apiUrl, username, password, credentials string) error {
+ registryLock.Lock()
+ defer registryLock.Unlock()
+ registries[prefix] = &RegistryEndpoint{
+ RegistryName: name,
+ RegistryPrefix: prefix,
+ RegistryAPI: apiUrl,
+ Username: username,
+ Password: password,
+ Credentials: credentials,
+ }
+ return nil
+}
+
+// GetRegistryEndpoint retrieves the endpoint information for the given prefix
+func GetRegistryEndpoint(prefix string) (*RegistryEndpoint, error) {
+ registryLock.RLock()
+ defer registryLock.RUnlock()
+ if registry, ok := registries[prefix]; ok {
+ return registry, nil
+ } else {
+ return nil, fmt.Errorf("no registry with prefix '%s' configured", prefix)
+ }
+}
+
+// SetRegistryEndpointCredentials allows to change the credentials used for
+// endpoint access for existing RegistryEndpoint configuration
+func SetRegistryEndpointCredentials(prefix, username, password string) error {
+ registry, err := GetRegistryEndpoint(prefix)
+ if err != nil {
+ return err
+ }
+ registry.lock.Lock()
+ registry.Username = username
+ registry.Password = password
+ registry.lock.Unlock()
+ return nil
+}
diff --git a/pkg/registry/endpoints_test.go b/pkg/registry/endpoints_test.go
new file mode 100644
index 0000000..48fd4b4
--- /dev/null
+++ b/pkg/registry/endpoints_test.go
@@ -0,0 +1,98 @@
+package registry
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_GetEndpoints(t *testing.T) {
+ t.Run("Get default endpoint", func(t *testing.T) {
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ assert.Equal(t, ep.RegistryPrefix, "")
+ })
+ t.Run("Get GCR endpoint", func(t *testing.T) {
+ ep, err := GetRegistryEndpoint("gcr.io")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ assert.Equal(t, ep.RegistryPrefix, "gcr.io")
+ })
+
+ t.Run("Get non-existing endpoint", func(t *testing.T) {
+ ep, err := GetRegistryEndpoint("foobar.com")
+ assert.Error(t, err)
+ assert.Nil(t, ep)
+ })
+}
+
+func Test_AddEndpoint(t *testing.T) {
+ t.Run("Add new endpoint", func(t *testing.T) {
+ err := AddRegistryEndpoint("example.com", "Example", "https://example.com", "", "", "")
+ require.NoError(t, err)
+ })
+ t.Run("Get example.com endpoint", func(t *testing.T) {
+ ep, err := GetRegistryEndpoint("example.com")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ assert.Equal(t, ep.RegistryPrefix, "example.com")
+ assert.Equal(t, ep.RegistryName, "Example")
+ assert.Equal(t, ep.RegistryAPI, "https://example.com")
+ })
+ t.Run("Change existing endpoint", func(t *testing.T) {
+ err := AddRegistryEndpoint("example.com", "Example", "https://example.com", "", "", "")
+ require.NoError(t, err)
+ })
+}
+
+func Test_SetEndpointCredentials(t *testing.T) {
+ t.Run("Set credentials on default registry", func(t *testing.T) {
+ err := SetRegistryEndpointCredentials("", "username", "password")
+ require.NoError(t, err)
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ assert.Equal(t, ep.Username, "username")
+ assert.Equal(t, ep.Password, "password")
+ })
+
+ t.Run("Unset credentials on default registry", func(t *testing.T) {
+ err := SetRegistryEndpointCredentials("", "", "")
+ require.NoError(t, err)
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ assert.Equal(t, ep.Username, "")
+ assert.Equal(t, ep.Password, "")
+ })
+}
+
+func Test_EndpointConcurrentAccess(t *testing.T) {
+ // Make sure we're not deadlocking on read
+ t.Run("Concurrent read access", func(t *testing.T) {
+ for i := 0; i < 50; i++ {
+ go func() {
+ ep, err := GetRegistryEndpoint("gcr.io")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ }()
+ }
+ })
+ // Make sure we're not deadlocking on write
+ t.Run("Concurrent write access", func(t *testing.T) {
+ for i := 0; i < 50; i++ {
+ go func(i int) {
+ username := fmt.Sprintf("Username-%d", i)
+ password := fmt.Sprintf("Password-%d", i)
+ err := SetRegistryEndpointCredentials("", username, password)
+ require.NoError(t, err)
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ require.NotNil(t, ep)
+ }(i)
+ }
+ })
+}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
new file mode 100644
index 0000000..da967de
--- /dev/null
+++ b/pkg/registry/registry.go
@@ -0,0 +1,69 @@
+package registry
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/argoproj-labs/argocd-image-updater/pkg/client"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/image"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+
+ "github.com/nokia/docker-registry-client/registry"
+)
+
+// GetTags returns a list of available tags for the given image
+func (clientInfo *RegistryEndpoint) GetTags(img *image.ContainerImage, kubeClient *client.KubernetesClient) ([]string, error) {
+ err := clientInfo.setEndpointCredentials(kubeClient)
+ if err != nil {
+ return nil, err
+ }
+ client, err := registry.NewCustom(clientInfo.RegistryAPI, registry.Options{
+ DoInitialPing: clientInfo.Ping,
+ Logf: registry.Quiet,
+ Username: clientInfo.Username,
+ Password: clientInfo.Password,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // DockerHub has a special namespace 'library', that is hidden from the user
+ var nameInRegistry string
+ if len := len(strings.Split(img.ImageName, "/")); len == 1 {
+ nameInRegistry = "library/" + img.ImageName
+ } else {
+ nameInRegistry = img.ImageName
+ }
+ tags, err := client.Tags(nameInRegistry)
+ if err != nil {
+ return nil, err
+ }
+ return tags, err
+}
+
+func (clientInfo *RegistryEndpoint) setEndpointCredentials(kubeClient *client.KubernetesClient) error {
+ if clientInfo.Username == "" && clientInfo.Password == "" && clientInfo.Credentials != "" {
+ credSrc, err := image.ParseCredentialSource(clientInfo.Credentials, false)
+ if err != nil {
+ return err
+ }
+
+ // For fetching credentials, we must have working Kubernetes client.
+ if (credSrc.Type == image.CredentialSourcePullSecret || credSrc.Type == image.CredentialSourceSecret) && kubeClient == nil {
+ log.WithContext().
+ AddField("registry", clientInfo.RegistryAPI).
+ Warnf("cannot user K8s credentials without Kubernetes client")
+ return fmt.Errorf("could not fetch image tags")
+ }
+
+ creds, err := credSrc.FetchCredentials(clientInfo.RegistryAPI, kubeClient)
+ if err != nil {
+ return err
+ }
+
+ clientInfo.Username = creds.Username
+ clientInfo.Password = creds.Password
+ }
+
+ return nil
+}
diff --git a/pkg/version/version.go b/pkg/version/version.go
new file mode 100644
index 0000000..72069d2
--- /dev/null
+++ b/pkg/version/version.go
@@ -0,0 +1,28 @@
+package version
+
+import "fmt"
+
+const (
+ majorVersion = "0"
+ minorVersion = "0"
+ patchVersion = "1"
+ preReleaseString = ""
+)
+
+const binaryName = "argocd-image-updater"
+
+func Version() string {
+ version := fmt.Sprintf("v%s.%s.%s", majorVersion, minorVersion, patchVersion)
+ if preReleaseString != "" {
+ version += fmt.Sprintf("-%s", preReleaseString)
+ }
+ return version
+}
+
+func BinaryName() string {
+ return binaryName
+}
+
+func Useragent() string {
+ return fmt.Sprintf("%s %s", BinaryName(), Version())
+}
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..909e5a9
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,9 @@
+# What lives here
+
+The `test/` directory does not contain any tests, but all fixtures and data
+for running the unit tests.
+
+Do not add unit tests here. If a test-specific method would be useful to more
+than one package's unit test, add it to the `fixture` package. Methods defined
+as fixture are allowed to `panic()`, so they must not be used in code outside
+the unit tests.
diff --git a/test/fake/kubernetes.go b/test/fake/kubernetes.go
new file mode 100644
index 0000000..cad8b50
--- /dev/null
+++ b/test/fake/kubernetes.go
@@ -0,0 +1,16 @@
+package fake
+
+import (
+ "k8s.io/apimachinery/pkg/runtime"
+ kubefake "k8s.io/client-go/kubernetes/fake"
+)
+
+func NewFakeKubeClient() *kubefake.Clientset {
+ clientset := kubefake.NewSimpleClientset()
+ return clientset
+}
+
+func NewFakeClientsetWithResources(objects ...runtime.Object) *kubefake.Clientset {
+ clientset := kubefake.NewSimpleClientset(objects...)
+ return clientset
+}
diff --git a/test/fixture/capture.go b/test/fixture/capture.go
new file mode 100644
index 0000000..57073bb
--- /dev/null
+++ b/test/fixture/capture.go
@@ -0,0 +1,55 @@
+package fixture
+
+import (
+ "io/ioutil"
+ "os"
+)
+
+func CaptureStdout(callback func()) (string, error) {
+ oldStdout := os.Stdout
+ oldStderr := os.Stderr
+ r, w, err := os.Pipe()
+ if err != nil {
+ return "", err
+ }
+ os.Stdout = w
+ defer func() {
+ os.Stdout = oldStdout
+ os.Stderr = oldStderr
+ }()
+
+ callback()
+
+ w.Close()
+
+ data, err := ioutil.ReadAll(r)
+
+ if err != nil {
+ return "", err
+ }
+ return string(data), err
+}
+
+func CaptureStderr(callback func()) (string, error) {
+ oldStdout := os.Stdout
+ oldStderr := os.Stderr
+ r, w, err := os.Pipe()
+ if err != nil {
+ return "", err
+ }
+ os.Stderr = w
+ defer func() {
+ os.Stdout = oldStdout
+ os.Stderr = oldStderr
+ }()
+
+ callback()
+ w.Close()
+
+ data, err := ioutil.ReadAll(r)
+
+ if err != nil {
+ return "", err
+ }
+ return string(data), err
+}
diff --git a/test/fixture/fileutil.go b/test/fixture/fileutil.go
new file mode 100644
index 0000000..ce75911
--- /dev/null
+++ b/test/fixture/fileutil.go
@@ -0,0 +1,14 @@
+package fixture
+
+// Fixture functions for tests related to files
+
+import "io/ioutil"
+
+// MustReadFile must read a file from given path. Panics if it can't.
+func MustReadFile(path string) string {
+ retBytes, err := ioutil.ReadFile(path)
+ if err != nil {
+ panic(err)
+ }
+ return string(retBytes)
+}
diff --git a/test/fixture/kubernetes.go b/test/fixture/kubernetes.go
new file mode 100644
index 0000000..49d61be
--- /dev/null
+++ b/test/fixture/kubernetes.go
@@ -0,0 +1,33 @@
+package fixture
+
+import (
+ "encoding/json"
+
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func NewSecret(namespace, name string, entries map[string][]byte) *v1.Secret {
+ secret := v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace,
+ Name: name,
+ },
+ Data: entries,
+ }
+ return &secret
+}
+
+func MustCreateSecretFromFile(filepath string) *v1.Secret {
+ jsonData := MustReadFile(filepath)
+ return MustCreateSecretFromJson(jsonData)
+}
+
+func MustCreateSecretFromJson(jsonData string) *v1.Secret {
+ var s v1.Secret
+ err := json.Unmarshal([]byte(jsonData), &s)
+ if err != nil {
+ panic(err)
+ }
+ return &s
+}
diff --git a/test/testdata/docker/invalid1-config.json b/test/testdata/docker/invalid1-config.json
new file mode 100644
index 0000000..244ff09
--- /dev/null
+++ b/test/testdata/docker/invalid1-config.json
@@ -0,0 +1,7 @@
+{
+ "auths": {
+ "https://registry-1.docker.io/v2/": {
+ "auth": "Zm9vOmJhcg=="
+ }
+ }
+}
diff --git a/test/testdata/docker/valid-config.json b/test/testdata/docker/valid-config.json
new file mode 100644
index 0000000..244ff09
--- /dev/null
+++ b/test/testdata/docker/valid-config.json
@@ -0,0 +1,7 @@
+{
+ "auths": {
+ "https://registry-1.docker.io/v2/": {
+ "auth": "Zm9vOmJhcg=="
+ }
+ }
+}
diff --git a/test/testdata/kubernetes/config b/test/testdata/kubernetes/config
new file mode 100644
index 0000000..2226e38
--- /dev/null
+++ b/test/testdata/kubernetes/config
@@ -0,0 +1,19 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURBVENDQWVtZ0F3SUJBZ0lKQU1yVlBMS3JqWGx1TUEwR0NTcUdTSWIzRFFFQkN3VUFNQmN4RlRBVEJnTlYKQkFNTURERXdMakUxTWk0eE9ETXVNVEFlRncweU1EQTFNalF4TURBeU1qbGFGdzB6TURBMU1qSXhNREF5TWpsYQpNQmN4RlRBVEJnTlZCQU1NRERFd0xqRTFNaTR4T0RNdU1UQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQCkFEQ0NBUW9DZ2dFQkFOeXhoWVZsLzBOUm43L2F4RjRaKytBNzJTcFZmbXVCNCs3QUFlREYxZXgvSVlFaDIzR2IKSzFuS25FUTZuNlgvemMyWVIveGdiekswSzJCV1paUENKME9WS2xWMklyckdtZ0xnMnpmTVB0RDZwNytFZEN4LwpIUW10MmRRUndrdHhyMFJqRUxFeUtZVE1BdHhnOFJUYi95TUVsY25iOEVFaGthZ2lGNUJKSTdreTlzYks1UlA2CkhJMUlMS2k1cE5hNGQ1YlhYNXFLeXM2dUFhY0V0OXQ3ZVdxMGtLamE0ckR5alZIYjk5WFg3QkV1SXZtVzV1WTAKMTI2UmVIT2UzUG1STGNEcGhrYVBncFZVN3Z3dmdGeldYT24xU1c3TFRXTG80K0p5UHpic3NjdFFRRjd1cWlKSgpwOFpWRURDTUluTFRDRURjRUlUdGxhdytyaXRFbS95SytQTUNBd0VBQWFOUU1FNHdIUVlEVlIwT0JCWUVGRDZmCktRdnljWUk3SzNLeFIreWdhL3o5ZDBnek1COEdBMVVkSXdRWU1CYUFGRDZmS1F2eWNZSTdLM0t4Uit5Z2EvejkKZDBnek1Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQURBWG5PYXRQNnh4cUFxbApiYXlWUEg5SDJZQk5nN3JYd3pETnpUM2JxRWswYVg4RnJDbWU0RkhhVXl6QmlISTZOSi9wbWk0TkxwblRxa2NVCnY3M1RXUmhsZjE4dzQzUzA2ZFNmSXpSank3cUhXR2dWRnRKRTcrTXRhcllNaCsydkRwMSs1by9ic21wZk5DaDcKOEhFOGlqb3lGRDYvYnFmdGIyeU5jZmJVNmF4Wll5VmYvSlpvd2grZUtnU21iQXNaZXRLWXZmSGVEWnE2SFFsaQpjZnZqUUdMMDNMSGxtSjdQNWlBSkJyVlE5MmZLS3pOejRXQWV1aktiYlB0TlBtTnVpL3EzR0cwU3o4bGJiNXpvCnRvNitJMzAwUUJReGpVd0Uvb1AvUXkyN0dMeER2aGFFTEl4V2c1R1R3Q3NDeHBGZXUzSm0yMDNNU3pOVHlJRDYKOUZTa3Rxaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+ server: https://127.0.0.1:16443
+ name: mock-cluster
+contexts:
+- context:
+ cluster: mock-cluster
+ user: admin
+ name: mock-cluster
+current-context: mock-cluster
+kind: Config
+preferences: {}
+users:
+- name: admin
+ user:
+ password: foobar
+ username: admin
diff --git a/test/testdata/resources/dummy-secret.json b/test/testdata/resources/dummy-secret.json
new file mode 100644
index 0000000..7055f67
--- /dev/null
+++ b/test/testdata/resources/dummy-secret.json
@@ -0,0 +1,12 @@
+{
+ "apiVersion": "v1",
+ "data": {
+ "namespace": "YXJnb2Nk"
+ },
+ "kind": "Secret",
+ "metadata": {
+ "name": "test-secret",
+ "namespace": "test-namespace"
+ }
+}
+