diff options
| author | jannfis <jann@mistrust.net> | 2020-08-04 19:45:46 +0200 |
|---|---|---|
| committer | jannfis <jann@mistrust.net> | 2020-08-04 19:45:46 +0200 |
| commit | bb184543e516f17c5801242645b5d77d0244c538 (patch) | |
| tree | 79913d38a3f4566a4547d0923452a625518437c3 | |
Initial commit
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"] @@ -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 @@ -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 Binary files differnew file mode 100644 index 0000000..b9cb18e --- /dev/null +++ b/docs/assets/logo.png 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 @@ -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 +) @@ -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 ¶m + } + } + 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), ®List) + 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" + } +} + |
