├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── feature │ ├── deployment.yaml │ └── service.yaml ├── production │ ├── deployment.yaml │ ├── ingress.yaml │ ├── service.yaml │ └── snapshot.diff └── staging │ ├── deployment.yaml │ ├── ingress.yaml │ ├── service.yaml │ └── snapshot.diff ├── go.mod ├── go.sum ├── kyml.go ├── pkg ├── cat │ ├── cat.go │ ├── cat_test.go │ ├── deduplicate.go │ ├── sort.go │ └── testdata │ │ ├── base │ │ ├── deployment-a.yaml │ │ ├── deployment-b.yaml │ │ └── service.yaml │ │ └── overlay-prod │ │ └── deployment-a.yaml ├── commands │ ├── cat │ │ ├── cat.go │ │ └── cat_test.go │ ├── commands.go │ ├── completion │ │ ├── completion.go │ │ └── completion_test.go │ ├── resolve │ │ ├── resolve.go │ │ ├── resolve_test.go │ │ ├── supported_kinds.go │ │ └── supported_kinds_test.go │ ├── test │ │ ├── test.go │ │ ├── test_test.go │ │ └── testdata │ │ │ ├── production │ │ │ ├── deployment.yaml │ │ │ └── service.yaml │ │ │ └── staging │ │ │ ├── deployment.yaml │ │ │ └── service.yaml │ └── tmpl │ │ ├── tmpl.go │ │ └── tmpl_test.go ├── diff │ ├── diff.go │ └── diff_test.go ├── fs │ ├── fake_file.go │ ├── fake_filesystem.go │ ├── file.go │ ├── filesystem.go │ ├── os_file.go │ └── os_filesystem.go ├── k8syaml │ ├── gvk.go │ ├── gvk_test.go │ ├── k8syaml.go │ └── k8syaml_test.go └── resolve │ ├── resolve.go │ └── resolve_test.go └── scripts ├── build.sh └── release.md /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.16 19 | - run: go mod download 20 | - uses: golangci/golangci-lint-action@v2.5.2 21 | with: 22 | version: v1.40.1 23 | - run: go test ./... 24 | - run: ./scripts/build.sh 25 | - if: ${{ env.KYML_RELEASE_VERSION }} 26 | uses: marvinpinto/action-automatic-releases@v1.2.1 27 | with: 28 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 29 | prerelease: false 30 | automatic_release_tag: "v${{ env.KYML_RELEASE_VERSION }}" 31 | files: bin/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | modules-download-mode: readonly 3 | linters: 4 | disable-all: true 5 | enable: 6 | - deadcode 7 | - errcheck 8 | - gofmt 9 | - govet 10 | - ineffassign 11 | - revive 12 | - staticcheck 13 | - structcheck 14 | - typecheck 15 | - unused 16 | - varcheck 17 | linters-settings: 18 | errcheck: 19 | check-type-assertions: true 20 | ignore: "" 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint", 3 | "go.lintFlags": [ 4 | "--fast" 5 | ], 6 | "go.vetOnSave": "off" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [20210610] 10 | 11 | ### Added 12 | 13 | - Added version for Apple Silicon aka darwin/arm64 (thanks [@genebean](https://github.com/genebean)). 14 | 15 | ## [20190906] 16 | 17 | ### Changed 18 | 19 | - Show special error message if experimental Docker features are required. The message also gives instructions on how to enable experimental features. 20 | 21 | ## [20190831] 22 | 23 | ### Added 24 | 25 | - Improved zsh completions. 26 | 27 | ## [20190103] 28 | 29 | ### Added 30 | 31 | - Added hint about `--update` option to error messages of `kyml test` command, which is shown when the snapshot file does not exist or the snapshot doesn't match. 32 | 33 | ## [20181227] 34 | 35 | ### Added 36 | 37 | - Cache resolved images in `kyml resolve`. If your manifests include the same image reference multiple times, kyml will only ask the registry once. 38 | - The command `kyml tmpl` now errors if the manifest contains a template key, which was not specified in the command flags. 39 | 40 | ### Fixed 41 | 42 | - Resource deduplication didn't correctly check for resource name. It does now. 43 | 44 | ## 20181226 45 | 46 | - First release. 47 | 48 | [unreleased]: https://github.com/frigus02/kyml/compare/v20210610...HEAD 49 | [20210610]: https://github.com/frigus02/kyml/compare/v20190906...v20210610 50 | [20190906]: https://github.com/frigus02/kyml/compare/v20190831...v20190906 51 | [20190831]: https://github.com/frigus02/kyml/compare/v20190103...v20190831 52 | [20190103]: https://github.com/frigus02/kyml/compare/v20181227...v20190103 53 | [20181227]: https://github.com/frigus02/kyml/compare/v20181226...v20181227 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | From opening a bug report to creating a pull request: every contribution is appreciated and welcome. If you're planning to implement a new feature or change a command please create an issue first. This way we can ensure that your precious work is not in vain. 4 | 5 | ## Process 6 | 7 | 1. Setup prerequisites on your machine. Currently the following are required: 8 | 9 | - go v1.13.x 10 | 11 | 1. Fork, then clone the repository. 12 | 13 | 1. Try to run the tests to see if everything works correctly. If you're doing this for the first time, go should automatically download the necessary dependencies. 14 | 15 | ```sh 16 | go test ./... 17 | ``` 18 | 19 | 1. Create a new branch based on master and start to make your changes. 20 | 21 | ```sh 22 | git checkout -b my-feature 23 | ``` 24 | 25 | 1. Once you're happy, push your branch and [open a pull request](https://github.com/frigus02/kyml/compare) ([help](https://help.github.com/articles/creating-a-pull-request/)). 26 | 27 | ```sh 28 | git push origin -u my-feature 29 | ``` 30 | 31 | ## Additional notes 32 | 33 | Some things that will increase the chance that your pull request is accepted: 34 | 35 | - Write tests 36 | - Follow the existing coding style 37 | - Write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Jan Kühle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kyml - Kubernetes YAML 2 | 3 | A CLI, which helps you to work with and deploy plain Kubernetes YAML files. 4 | 5 | ## Background 6 | 7 | There are many great tools out there to manage Kubernetes manifests, e.g. [ksonnet](https://ksonnet.io/) or [kustomize](https://github.com/kubernetes-sigs/kustomize). They try to make working with manifests easier by deduplicating config. However they usually introduce other configuration files, which comes with complexity on its own. I wanted something simpler, especially for smaller applications. 8 | 9 | So here is `kyml`: 10 | 11 | - Work with plain Kubernetes YAML files. No additional config files. 12 | - Duplicate files for each environment. But ensure updates always happen to all environments. 13 | - Support dynamic values with limited templating. 14 | - Save to run. Never touch original YAML files. 15 | 16 | ## Install 17 | 18 | ### macOS 19 | 20 | ```sh 21 | brew install frigus02/tap/kyml 22 | ``` 23 | 24 | ### Linux & Windows 25 | 26 | Download a binary from the [release page](https://github.com/frigus02/kyml/releases). 27 | 28 | This downloads the latest version for Linux: 29 | 30 | ```sh 31 | curl -sfL -o /usr/local/bin/kyml https://github.com/frigus02/kyml/releases/download/v20210610/kyml_20210610_linux_amd64 && chmod +x /usr/local/bin/kyml 32 | ``` 33 | 34 | ## Usage 35 | 36 | `kyml` provides commands for concatenating YAML files, testing and templating. Commands usually read manifests from stdin and print them to stdout. This allows you to build a pipeline of commands, which can end with a `kubectl apply` to do the deployment. 37 | 38 | - [Structure your manifests in the way you want](#structure-your-manifests-in-the-way-you-want) 39 | - [`kyml cat` - concatenate YAML files](#kyml-cat---concatenate-yaml-files) 40 | - [`kyml test` - ensure updates always happen to all environments](#kyml-test---ensure-updates-always-happen-to-all-environments) 41 | - [`kyml tmpl` - inject dynamic values](#kyml-tmpl---inject-dynamic-values) 42 | - [`kyml resolve` - resolve Docker images to their digest](#kyml-resolve---resolve-docker-images-to-their-digest) 43 | 44 | Run `kyml --help` for details about the different commands. 45 | 46 | ### Structure your manifests in the way you want 47 | 48 | For most of the examples in this readme we assume the following structure: 49 | 50 | ``` 51 | manifests 52 | |- staging 53 | | |- deployment.yaml 54 | | |- ingress.yaml 55 | | `- service.yaml 56 | `- production 57 | |- deployment.yaml 58 | |- ingress.yaml 59 | `- service.yaml 60 | ``` 61 | 62 | And some of them use this: 63 | 64 | ``` 65 | manifests 66 | |- base 67 | | |- ingress.yaml 68 | | `- service.yaml 69 | `- overlays 70 | |- staging 71 | | `- deployment.yaml 72 | `- production 73 | `- deployment.yaml 74 | ``` 75 | 76 | You can adapt these or use anything else, that makes sense for your application. 77 | 78 | ### `kyml cat` - concatenate YAML files 79 | 80 | Concatenate your files and pipe them into [`kubectl apply`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#apply) to deploy them. This does 2 things: 81 | 82 | - If multiple files contain the same Kubernetes resource, `kyml cat` deduplicates them. Only the one specified last makes it into the output. 83 | - Resources are sorted by dependencies. So even if you specify the namespace last (e.g. `kyml cat deployment.yaml namespace.yaml`) the namespace will appear first in the output. This makes sure your resources are created in the correct order. 84 | 85 | ```sh 86 | kyml cat manifests/production/* | kubectl apply -f - 87 | ``` 88 | 89 | ```sh 90 | kyml cat manifests/base/* manifests/overlays/production/* | kubectl apply -f - 91 | ``` 92 | 93 | ### `kyml test` - ensure updates always happen to all environments 94 | 95 | Testing works by creating a diff between two environments and storing it in a snapshot file. The command compares the diff result to the snapshot and fails if it doesn't match. 96 | 97 | `kyml test` reads manifests of the main environment from stdin and files from the comparison environment are specified as arguments, similar to `kyml cat`. If the snapshot matches, it prints the main environment manifests to stdout. This way you can include a test in your deployment command pipeline to make sure nothing gets deployed if the test fails. 98 | 99 | ```sh 100 | kyml cat manifests/production/* | 101 | kyml test manifests/staging/* \ 102 | --name-main production \ 103 | --name-comparison staging \ 104 | --snapshot-file tests/snapshot-production-vs-staging.diff | 105 | kubectl apply -f - 106 | ``` 107 | 108 | ### `kyml tmpl` - inject dynamic values 109 | 110 | Use templates (in the [go template](https://golang.org/pkg/text/template/) syntax) to inject dynamic values. To make sure values are escaped properly and this feature doesn't get misused you can only template string scalars. Example: 111 | 112 | ```yaml 113 | apiVersion: v1 114 | kind: Namespace 115 | metadata: 116 | name: the-namespace 117 | labels: 118 | branch: "{{.TRAVIS_BRANCH}}" 119 | ``` 120 | 121 | `kyml test` reads manifests from stdin and prints the result to stdout. Values are provided as command line options. Use `--value key=value` for literal strings and `--env ENV_VAR` for environment variables. These options can be repeated multiple times. The command fails if the manifests contain any template key, which is not specified on the command line. 122 | 123 | ```sh 124 | kyml cat manifests/production/* | 125 | kyml test manifests/staging/* \ 126 | --name-main production \ 127 | --name-comparison staging \ 128 | --snapshot-file tests/snapshot-production-vs-staging.diff | 129 | kyml tmpl \ 130 | -v Greeting=hello \ 131 | -v ImageTag=$(git rev-parse --short HEAD) \ 132 | -e TRAVIS_BRANCH | 133 | kubectl apply -f - 134 | ``` 135 | 136 | ### `kyml resolve` - resolve Docker images to their digest 137 | 138 | If you tag the same image multiple times (e.g. because you build every commit and tag images with the commit sha), you may want to resolve the tags to the image digest. This way Kubernetes only restarts your applications if the image content has changed. 139 | 140 | ```sh 141 | kyml cat manifests/production/* | 142 | kyml tmpl -v ImageTag=$(git rev-parse --short HEAD) | 143 | kyml resolve | 144 | kubectl apply -f - 145 | ``` 146 | 147 | ## Contributing 148 | 149 | Please see [CONTRIBUTING.md](CONTRIBUTING.md). 150 | 151 | ## License 152 | 153 | [MIT](LICENSE) 154 | -------------------------------------------------------------------------------- /examples/feature/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: the-deployment-{{.TRAVIS_BRANCH}} 5 | labels: 6 | app: hello 7 | branch: "{{.TRAVIS_BRANCH}}" 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: hello 13 | template: 14 | metadata: 15 | labels: 16 | app: hello 17 | branch: "{{.TRAVIS_BRANCH}}" 18 | spec: 19 | containers: 20 | - name: the-container 21 | image: nginxdemos/hello:{{.ImageTag}} 22 | ports: 23 | - containerPort: 80 24 | env: 25 | - name: GREETING 26 | value: "{{.Greeting}}" 27 | - name: DB_CONNECTION_STRING 28 | value: "User ID=root;Host=the-test-db-server;Port=5432;Database=the-db;" 29 | - name: DEBUG 30 | value: "true" 31 | -------------------------------------------------------------------------------- /examples/feature/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service-{{.TRAVIS_BRANCH}} 5 | labels: 6 | app: hello 7 | branch: "{{.TRAVIS_BRANCH}}" 8 | spec: 9 | selector: 10 | deployment: hello 11 | type: LoadBalancer 12 | ports: 13 | - protocol: TCP 14 | port: 80 15 | targetPort: 80 16 | -------------------------------------------------------------------------------- /examples/production/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: the-deployment 5 | labels: 6 | app: hello 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: hello 12 | template: 13 | metadata: 14 | labels: 15 | app: hello 16 | spec: 17 | containers: 18 | - name: the-container 19 | image: nginxdemos/hello:{{.ImageTag}} 20 | ports: 21 | - containerPort: 80 22 | env: 23 | - name: GREETING 24 | value: "{{.Greeting}}" 25 | - name: DB_CONNECTION_STRING 26 | value: "User ID=root;Host=the-db-server;Port=5432;Database=the-db;" 27 | -------------------------------------------------------------------------------- /examples/production/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: the-ingress 5 | labels: 6 | app: hello 7 | annotations: 8 | nginx.ingress.kubernetes.io/rewrite-target: / 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - path: /the-path 14 | backend: 15 | serviceName: the-service 16 | servicePort: 80 17 | -------------------------------------------------------------------------------- /examples/production/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service 5 | labels: 6 | app: hello 7 | spec: 8 | selector: 9 | deployment: hello 10 | type: LoadBalancer 11 | ports: 12 | - protocol: TCP 13 | port: 80 14 | targetPort: 80 15 | -------------------------------------------------------------------------------- /examples/production/snapshot.diff: -------------------------------------------------------------------------------- 1 | --- production 2 | +++ staging 3 | @@ -38 +38,3 @@ 4 | - value: User ID=root;Host=the-db-server;Port=5432;Database=the-db; 5 | + value: User ID=root;Host=the-test-db-server;Port=5432;Database=the-db; 6 | + - name: DEBUG 7 | + value: "true" 8 | -------------------------------------------------------------------------------- /examples/staging/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: the-deployment 5 | labels: 6 | app: hello 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: hello 12 | template: 13 | metadata: 14 | labels: 15 | app: hello 16 | spec: 17 | containers: 18 | - name: the-container 19 | image: nginxdemos/hello:{{.ImageTag}} 20 | ports: 21 | - containerPort: 80 22 | env: 23 | - name: GREETING 24 | value: "{{.Greeting}}" 25 | - name: DB_CONNECTION_STRING 26 | value: "User ID=root;Host=the-test-db-server;Port=5432;Database=the-db;" 27 | - name: DEBUG 28 | value: "true" 29 | -------------------------------------------------------------------------------- /examples/staging/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: the-ingress 5 | labels: 6 | app: hello 7 | annotations: 8 | nginx.ingress.kubernetes.io/rewrite-target: / 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - path: /the-path 14 | backend: 15 | serviceName: the-service 16 | servicePort: 80 17 | -------------------------------------------------------------------------------- /examples/staging/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service 5 | labels: 6 | app: hello 7 | spec: 8 | selector: 9 | deployment: hello 10 | type: LoadBalancer 11 | ports: 12 | - protocol: TCP 13 | port: 80 14 | targetPort: 80 15 | -------------------------------------------------------------------------------- /examples/staging/snapshot.diff: -------------------------------------------------------------------------------- 1 | --- staging 2 | +++ feature 3 | @@ -7 +7,2 @@ 4 | - name: the-service 5 | + branch: '{{.TRAVIS_BRANCH}}' 6 | + name: the-service-{{.TRAVIS_BRANCH}} 7 | @@ -22 +23,2 @@ 8 | - name: the-deployment 9 | + branch: '{{.TRAVIS_BRANCH}}' 10 | + name: the-deployment-{{.TRAVIS_BRANCH}} 11 | @@ -24 +26 @@ 12 | - replicas: 2 13 | + replicas: 1 14 | @@ -31,0 +34 @@ 15 | + branch: '{{.TRAVIS_BRANCH}}' 16 | @@ -45,17 +47,0 @@ 17 | ---- 18 | -apiVersion: extensions/v1beta1 19 | -kind: Ingress 20 | -metadata: 21 | - annotations: 22 | - nginx.ingress.kubernetes.io/rewrite-target: / 23 | - labels: 24 | - app: hello 25 | - name: the-ingress 26 | -spec: 27 | - rules: 28 | - - http: 29 | - paths: 30 | - - backend: 31 | - serviceName: the-service 32 | - servicePort: 80 33 | - path: /the-path 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/frigus02/kyml 2 | 3 | require ( 4 | github.com/google/go-cmp v0.5.6 // indirect 5 | github.com/google/gofuzz v1.2.0 // indirect 6 | github.com/json-iterator/go v1.1.11 // indirect 7 | github.com/pmezard/go-difflib v1.0.0 8 | github.com/spf13/cobra v1.1.3 9 | golang.org/x/net v0.23.0 // indirect 10 | k8s.io/apimachinery v0.21.2 11 | k8s.io/klog/v2 v2.9.0 // indirect 12 | sigs.k8s.io/structured-merge-diff/v4 v4.1.1 // indirect 13 | sigs.k8s.io/yaml v1.2.0 14 | ) 15 | 16 | go 1.16 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 18 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 19 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 20 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 21 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 22 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 23 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 24 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 25 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 26 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 27 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 28 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 29 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 30 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 31 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 32 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 33 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 34 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 35 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 36 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 37 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 38 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 39 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 44 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 45 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 46 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 47 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 48 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 49 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 50 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 51 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 52 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 53 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 54 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 55 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 56 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 57 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 58 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 59 | github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= 60 | github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 61 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 62 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 63 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 64 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 65 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 66 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 67 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 68 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 69 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 70 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 71 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 72 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 73 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 74 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 75 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 76 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 77 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 78 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 79 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 80 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 81 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 82 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 83 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 84 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 85 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 86 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 87 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 88 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 89 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 91 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 92 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 93 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 94 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 95 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 99 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 100 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 101 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 102 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 103 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 104 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 105 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 106 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 107 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 108 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 109 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 110 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 111 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 112 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= 113 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 114 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 115 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 116 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 117 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 118 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 119 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 120 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 121 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 122 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 123 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 124 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 125 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 126 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 127 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 128 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 129 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 130 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 131 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 132 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 133 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 134 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 135 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 136 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 137 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 138 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 139 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 140 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 141 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 142 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 143 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 144 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 145 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 146 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 147 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 148 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 149 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 150 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 151 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 152 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 153 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 154 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 155 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 156 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 157 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 158 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 159 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 160 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 161 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 162 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 163 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 164 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 165 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 166 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 167 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 168 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 169 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 170 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 171 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 172 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 173 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 174 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 175 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 176 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 177 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 178 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 179 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 180 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 181 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 182 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 183 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 184 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 185 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 186 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 187 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 188 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 189 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 190 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 191 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 192 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 193 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 194 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 195 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 196 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 197 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 198 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 199 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 200 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 201 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 202 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 203 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 204 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 205 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 206 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 207 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 208 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 209 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 210 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 211 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 212 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 213 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 214 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 215 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 216 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 217 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 218 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 219 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 220 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 221 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 222 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 223 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 224 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 225 | github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= 226 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 227 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 228 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 229 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 230 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 231 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 232 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 233 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 234 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 235 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 236 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 237 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 238 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 239 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 240 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 241 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 242 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 243 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 244 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 245 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 246 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 247 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 248 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 249 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 250 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 251 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 252 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 253 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 254 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 255 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 256 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 257 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 258 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 259 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 260 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 261 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 262 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 263 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 264 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 265 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 266 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 267 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 268 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 269 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 270 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 271 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 272 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 273 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 274 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 275 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 276 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 277 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 278 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 279 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 280 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 281 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 282 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 283 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 284 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 285 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 286 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 287 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 288 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 289 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 290 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 291 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 292 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 293 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 294 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 295 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 296 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 297 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 298 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 299 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 300 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 301 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 302 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 303 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 304 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 305 | golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 306 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 307 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 308 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 309 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 310 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 311 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 312 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 313 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 314 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 315 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 316 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 318 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 319 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 320 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 321 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 325 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 326 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 327 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 328 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 330 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 331 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 332 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 333 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 345 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 346 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 347 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 348 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 349 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 350 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 351 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 352 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 353 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 354 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 355 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 356 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 357 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 358 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 359 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 360 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 361 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 362 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 363 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 364 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 365 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 366 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 367 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 368 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 369 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 370 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 371 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 372 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 373 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 374 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 375 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 376 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 377 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 378 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 379 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 380 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 381 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 382 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 383 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 384 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 385 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 386 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 387 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 388 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 389 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 390 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 391 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 392 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 393 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 394 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 395 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 396 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 397 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 398 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 399 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 400 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 401 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 402 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 403 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 404 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 405 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 406 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 407 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 408 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 409 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 410 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 411 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 412 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 413 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 414 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 415 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 416 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 417 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 418 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 419 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 420 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 421 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 422 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 423 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 424 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 425 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 426 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 427 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 428 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 429 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 430 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 431 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 432 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 433 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 434 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 435 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 436 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 437 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 438 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 439 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 440 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 441 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 442 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 443 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 444 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 445 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 446 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 447 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 448 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 449 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 450 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 451 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 452 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 453 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 454 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 455 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 456 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 457 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 458 | k8s.io/apimachinery v0.21.2 h1:vezUc/BHqWlQDnZ+XkrpXSmnANSLbpnlpwo0Lhk0gpc= 459 | k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= 460 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 461 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 462 | k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= 463 | k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= 464 | k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= 465 | k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= 466 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 467 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 468 | sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 469 | sigs.k8s.io/structured-merge-diff/v4 v4.1.1 h1:nYqY2A6oy37sKLYuSBXuQhbj4JVclzJK13BOIvJG5XU= 470 | sigs.k8s.io/structured-merge-diff/v4 v4.1.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 471 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 472 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 473 | -------------------------------------------------------------------------------- /kyml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/frigus02/kyml/pkg/commands" 7 | ) 8 | 9 | func main() { 10 | if err := commands.NewRootCommand().Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | 14 | os.Exit(0) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/cat/cat.go: -------------------------------------------------------------------------------- 1 | package cat 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/frigus02/kyml/pkg/fs" 7 | "github.com/frigus02/kyml/pkg/k8syaml" 8 | 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | ) 11 | 12 | // Cat reads YAML documents from the specified files and prints them one after 13 | // another in the specified writer. If a YAML document has the same apiVersion, 14 | // kind, namespace and name as a previous one it replaces it in the output. 15 | func Cat(out io.Writer, files []string, fs fs.Filesystem) error { 16 | var documents []*unstructured.Unstructured 17 | for _, filename := range files { 18 | file, err := fs.Open(filename) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | docsInFile, err := k8syaml.Decode(file) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | err = file.Close() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | documents = addOrReplaceExistingDocs(documents, docsInFile) 34 | } 35 | 36 | sortDocs(documents) 37 | 38 | return k8syaml.Encode(out, documents) 39 | } 40 | 41 | // Stream reads YAML documents from the specified reader and prints them one 42 | // after another in the specified writer. If a YAML document has the same 43 | // apiVersion, kind, namespace and name as a previous one it replaces it in the 44 | // output. 45 | func Stream(out io.Writer, stream io.Reader) error { 46 | documents, err := StreamDecodeOnly(stream) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return k8syaml.Encode(out, documents) 52 | } 53 | 54 | // StreamDecodeOnly works like Stream, but returns a slice of unstructured 55 | // objects instead of writing them to an output. 56 | func StreamDecodeOnly(stream io.Reader) ([]*unstructured.Unstructured, error) { 57 | docsInStream, err := k8syaml.Decode(stream) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | var documents []*unstructured.Unstructured 63 | documents = addOrReplaceExistingDocs(documents, docsInStream) 64 | 65 | sortDocs(documents) 66 | 67 | return documents, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/cat/cat_test.go: -------------------------------------------------------------------------------- 1 | package cat 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/frigus02/kyml/pkg/fs" 10 | ) 11 | 12 | var testDataManifests = `--- 13 | apiVersion: v1 14 | kind: Service 15 | metadata: 16 | name: the-service 17 | spec: 18 | ports: 19 | - port: 80 20 | protocol: TCP 21 | selector: 22 | deployment: hello 23 | type: LoadBalancer 24 | --- 25 | apiVersion: apps/v1 26 | kind: Deployment 27 | metadata: 28 | name: deployment-a 29 | spec: 30 | replicas: 3 31 | template: 32 | spec: 33 | containers: 34 | - image: kyml/hello 35 | name: the-container 36 | --- 37 | apiVersion: apps/v1 38 | kind: Deployment 39 | metadata: 40 | name: deployment-b 41 | spec: 42 | replicas: 1 43 | template: 44 | spec: 45 | containers: 46 | - image: kyml/hello 47 | name: the-container 48 | ` 49 | 50 | func mustCreateFs(t *testing.T) fs.Filesystem { 51 | fsWithTestdata, err := fs.NewFakeFilesystemFromDisk( 52 | "testdata/base/deployment-a.yaml", 53 | "testdata/base/deployment-b.yaml", 54 | "testdata/base/service.yaml", 55 | "testdata/overlay-prod/deployment-a.yaml", 56 | ) 57 | if err != nil { 58 | t.Fatalf("error reading testdata: %v", err) 59 | } 60 | 61 | return fsWithTestdata 62 | } 63 | 64 | func mustCreateStream(t *testing.T) io.Reader { 65 | var content []byte 66 | files := []string{ 67 | "testdata/base/deployment-a.yaml", 68 | "testdata/base/deployment-b.yaml", 69 | "testdata/base/service.yaml", 70 | "testdata/overlay-prod/deployment-a.yaml", 71 | } 72 | 73 | for _, file := range files { 74 | data, err := ioutil.ReadFile(file) 75 | if err != nil { 76 | t.Fatalf("error reading testdata: %v", err) 77 | } 78 | 79 | content = append(content, []byte("\n---\n")...) 80 | content = append(content, data...) 81 | } 82 | 83 | return bytes.NewReader(content) 84 | } 85 | 86 | func TestCat(t *testing.T) { 87 | type args struct { 88 | files []string 89 | fs fs.Filesystem 90 | } 91 | tests := []struct { 92 | name string 93 | args args 94 | wantOut string 95 | wantErr bool 96 | }{ 97 | { 98 | name: "print deduplicated and sorted docs from files", 99 | args: args{ 100 | files: []string{ 101 | "testdata/base/deployment-a.yaml", 102 | "testdata/base/deployment-b.yaml", 103 | "testdata/base/service.yaml", 104 | "testdata/overlay-prod/deployment-a.yaml", 105 | }, 106 | fs: mustCreateFs(t), 107 | }, 108 | wantOut: testDataManifests, 109 | wantErr: false, 110 | }, 111 | { 112 | name: "file does not exist", 113 | args: args{ 114 | files: []string{ 115 | "testdata/something.yaml", 116 | }, 117 | fs: mustCreateFs(t), 118 | }, 119 | wantOut: "", 120 | wantErr: true, 121 | }, 122 | } 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | out := &bytes.Buffer{} 126 | if err := Cat(out, tt.args.files, tt.args.fs); (err != nil) != tt.wantErr { 127 | t.Errorf("Cat() error = %v, wantErr %v", err, tt.wantErr) 128 | return 129 | } 130 | if gotOut := out.String(); gotOut != tt.wantOut { 131 | t.Errorf("Cat() = %v, want %v", gotOut, tt.wantOut) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func TestStream(t *testing.T) { 138 | type args struct { 139 | stream io.Reader 140 | } 141 | tests := []struct { 142 | name string 143 | args args 144 | wantOut string 145 | wantErr bool 146 | }{ 147 | { 148 | name: "print deduplicated and sorted docs from stream", 149 | args: args{ 150 | stream: mustCreateStream(t), 151 | }, 152 | wantOut: testDataManifests, 153 | wantErr: false, 154 | }, 155 | } 156 | for _, tt := range tests { 157 | t.Run(tt.name, func(t *testing.T) { 158 | out := &bytes.Buffer{} 159 | if err := Stream(out, tt.args.stream); (err != nil) != tt.wantErr { 160 | t.Errorf("Stream() error = %v, wantErr %v", err, tt.wantErr) 161 | return 162 | } 163 | if gotOut := out.String(); gotOut != tt.wantOut { 164 | t.Errorf("Stream() = %v, want %v", gotOut, tt.wantOut) 165 | } 166 | }) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /pkg/cat/deduplicate.go: -------------------------------------------------------------------------------- 1 | package cat 2 | 3 | import ( 4 | "github.com/frigus02/kyml/pkg/k8syaml" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | func addOrReplaceExistingDocs(existingDocs, newDocs []*unstructured.Unstructured) []*unstructured.Unstructured { 10 | for _, doc := range newDocs { 11 | docGVK := doc.GroupVersionKind() 12 | found := false 13 | for i, seenDoc := range existingDocs { 14 | if k8syaml.GVKEquals(docGVK, seenDoc.GroupVersionKind()) && 15 | doc.GetNamespace() == seenDoc.GetNamespace() && 16 | doc.GetName() == seenDoc.GetName() { 17 | existingDocs[i] = doc 18 | found = true 19 | break 20 | } 21 | } 22 | 23 | if !found { 24 | existingDocs = append(existingDocs, doc) 25 | } 26 | } 27 | 28 | return existingDocs 29 | } 30 | -------------------------------------------------------------------------------- /pkg/cat/sort.go: -------------------------------------------------------------------------------- 1 | package cat 2 | 3 | import ( 4 | "sort" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | var gvkOrder = []schema.GroupVersionKind{ 11 | // Most resources require a namespace. A namespace has no requirements. 12 | {Group: "", Version: "v1", Kind: "Namespace"}, 13 | 14 | // Custom resources require the definition. 15 | {Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"}, 16 | 17 | // StorageClasses can be configured as default, so that PVCs can use them 18 | // without an explicit reference. 19 | {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}, 20 | 21 | // Creation of a service account fails if a secret referenced in 22 | // imagePullSecrets does not exist. 23 | {Group: "", Version: "v1", Kind: "ConfigMap"}, 24 | {Group: "", Version: "v1", Kind: "Secret"}, 25 | 26 | // Creation of pods fail if the service account referenced in 27 | // serviceAccountName does not exist. Role bindings require the referenced 28 | // service account and role. 29 | {Group: "", Version: "v1", Kind: "ServiceAccount"}, 30 | {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, 31 | {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, 32 | {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}, 33 | {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, 34 | 35 | // It’s best to specify the service first, since that will ensure the 36 | // scheduler can spread the pods associated with the service as they are 37 | // created by the controller(s), such as Deployment. 38 | // https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/ 39 | {Group: "", Version: "v1", Kind: "Service"}, 40 | 41 | // Several resources require pods, e.g. HorizontalPodAutoscaler. These 42 | // resources will create pods. 43 | {Group: "apps", Version: "v1", Kind: "DaemonSet"}, 44 | {Group: "apps", Version: "v1", Kind: "Deployment"}, 45 | {Group: "apps", Version: "v1", Kind: "ReplicaSet"}, 46 | {Group: "apps", Version: "v1", Kind: "StatefulSet"}, 47 | {Group: "batch", Version: "v1", Kind: "Job"}, 48 | {Group: "batch", Version: "v1beta1", Kind: "CronJob"}, 49 | {Group: "", Version: "v1", Kind: "ReplicationController"}, 50 | } 51 | var gvkOrderMap = func() map[string]int { 52 | m := map[string]int{} 53 | for i, n := range gvkOrder { 54 | m[n.String()] = i 55 | } 56 | return m 57 | }() 58 | 59 | func sortDocs(docs []*unstructured.Unstructured) { 60 | sort.SliceStable(docs, func(i, j int) bool { 61 | indexI, foundI := gvkOrderMap[docs[i].GroupVersionKind().String()] 62 | indexJ, foundJ := gvkOrderMap[docs[j].GroupVersionKind().String()] 63 | if foundI && foundJ { 64 | return indexI < indexJ 65 | } 66 | if foundI && !foundJ { 67 | return true 68 | } 69 | return false 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/cat/testdata/base/deployment-a.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: deployment-a 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: the-container 11 | image: kyml/hello 12 | -------------------------------------------------------------------------------- /pkg/cat/testdata/base/deployment-b.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: deployment-b 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: the-container 11 | image: kyml/hello 12 | -------------------------------------------------------------------------------- /pkg/cat/testdata/base/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service 5 | spec: 6 | selector: 7 | deployment: hello 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | -------------------------------------------------------------------------------- /pkg/cat/testdata/overlay-prod/deployment-a.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: deployment-a 5 | spec: 6 | replicas: 3 7 | template: 8 | spec: 9 | containers: 10 | - name: the-container 11 | image: kyml/hello 12 | -------------------------------------------------------------------------------- /pkg/commands/cat/cat.go: -------------------------------------------------------------------------------- 1 | package cat 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/frigus02/kyml/pkg/cat" 8 | "github.com/frigus02/kyml/pkg/fs" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type catOptions struct { 13 | files []string 14 | } 15 | 16 | // NewCmdCat creates a new cat command. 17 | func NewCmdCat(out io.Writer, fs fs.Filesystem) *cobra.Command { 18 | var o catOptions 19 | 20 | cmd := &cobra.Command{ 21 | Use: "cat ...", 22 | Short: "Concatenate Kubernetes YAML files to stdout", 23 | Long: `Read and concatenate YAML documents from all files in the order they are specified. Then print them to stdout. 24 | 25 | YAML documents are changed in the following ways: 26 | - Documents are parsed as Kubernetes YAML documents and then formatted. This will change indentation and ordering of properties. 27 | - Documents are deduplicated. If multiple YAML documents refer to the same Kubernetes resource, only the last one will appear in the result. 28 | - Documents are sorted by dependencies, e.g. namespaces come before deployments. 29 | 30 | The result of this command can be piped into other commands like "kyml test" or "kubectl apply".`, 31 | Example: ` # Cat one folder 32 | kyml cat production/* 33 | 34 | # Merge YAML documents from two folders 35 | kyml cat base/* overlay-production/* 36 | 37 | # Specify files individually 38 | kyml cat prod/deployment.yaml prod/service.yaml prod/ingress.yaml`, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | err := o.Validate(args) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return o.Run(out, fs) 46 | }, 47 | } 48 | 49 | // Cat supports infinite positional file arguments, however zsh completions 50 | // require each positional argument to be marked individually. We just mark 51 | // the first few. 52 | _ = cmd.MarkZshCompPositionalArgumentFile(1) 53 | _ = cmd.MarkZshCompPositionalArgumentFile(2) 54 | _ = cmd.MarkZshCompPositionalArgumentFile(3) 55 | _ = cmd.MarkZshCompPositionalArgumentFile(4) 56 | _ = cmd.MarkZshCompPositionalArgumentFile(5) 57 | 58 | return cmd 59 | } 60 | 61 | // Validate validates cat command. 62 | func (o *catOptions) Validate(args []string) error { 63 | if len(args) == 0 { 64 | return fmt.Errorf("specify at least one file") 65 | } 66 | 67 | o.files = args 68 | return nil 69 | } 70 | 71 | // Run runs cat command. 72 | func (o *catOptions) Run(out io.Writer, fs fs.Filesystem) error { 73 | return cat.Cat(out, o.files, fs) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/commands/cat/cat_test.go: -------------------------------------------------------------------------------- 1 | package cat 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_catOptions_Validate(t *testing.T) { 9 | type args struct { 10 | args []string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantErr bool 16 | wantFiles []string 17 | }{ 18 | { 19 | name: "error if no args", 20 | args: args{ 21 | args: []string{}, 22 | }, 23 | wantErr: true, 24 | wantFiles: nil, 25 | }, 26 | { 27 | name: "files set to args", 28 | args: args{ 29 | args: []string{"foo", "bar", "baz"}, 30 | }, 31 | wantErr: false, 32 | wantFiles: []string{"foo", "bar", "baz"}, 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | o := &catOptions{} 38 | if err := o.Validate(tt.args.args); (err != nil) != tt.wantErr { 39 | t.Errorf("catOptions.Validate() error = %v, wantErr %v", err, tt.wantErr) 40 | return 41 | } 42 | if !reflect.DeepEqual(o.files, tt.wantFiles) { 43 | t.Errorf("catOptions.files = %v, want %v", o.files, tt.wantFiles) 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/frigus02/kyml/pkg/commands/cat" 7 | "github.com/frigus02/kyml/pkg/commands/completion" 8 | "github.com/frigus02/kyml/pkg/commands/resolve" 9 | "github.com/frigus02/kyml/pkg/commands/test" 10 | "github.com/frigus02/kyml/pkg/commands/tmpl" 11 | "github.com/frigus02/kyml/pkg/fs" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var version = "dev" 16 | 17 | // NewRootCommand returns the root command for kyml. 18 | func NewRootCommand() *cobra.Command { 19 | osFs := fs.NewOSFilesystem() 20 | 21 | c := &cobra.Command{ 22 | Use: "kyml", 23 | Short: "A CLI, which helps you to work with and deploy plain Kubernetes YAML files.", 24 | SilenceUsage: true, 25 | Version: version, 26 | } 27 | 28 | c.AddCommand( 29 | cat.NewCmdCat(os.Stdout, osFs), 30 | completion.NewCmdCompletion(os.Stdout, c), 31 | resolve.NewCmdResolve(os.Stdin, os.Stdout), 32 | test.NewCmdTest(os.Stdin, os.Stdout, osFs), 33 | tmpl.NewCmdTmpl(os.Stdin, os.Stdout), 34 | ) 35 | 36 | return c 37 | } 38 | -------------------------------------------------------------------------------- /pkg/commands/completion/completion.go: -------------------------------------------------------------------------------- 1 | package completion 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type completionOptions struct { 11 | shell string 12 | } 13 | 14 | // NewCmdCompletion creates a new completion command. 15 | func NewCmdCompletion(out io.Writer, rootCommand *cobra.Command) *cobra.Command { 16 | var o completionOptions 17 | 18 | cmd := &cobra.Command{ 19 | Use: "completion ", 20 | Short: "Generate completion scripts for your shell", 21 | Long: `Write bash, powershell or zsh shell completion code for kyml to stdout. 22 | 23 | bash: Ensure you have bash completions installed and enabled. Then output to a file and load it from your .bash_profile. 24 | 25 | powershell: Ensure you have a PowerShell profile. Then output to a file and source it from the file provided by "$profile". 26 | 27 | zsh: Output to a file in a directory referenced by the $fpath shell variable.`, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | err := o.Validate(args) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return o.Run(out, rootCommand) 35 | }, 36 | ValidArgs: []string{"bash", "powershell", "zsh"}, 37 | } 38 | 39 | return cmd 40 | } 41 | 42 | // Validate validates completion command. 43 | func (o *completionOptions) Validate(args []string) error { 44 | if len(args) != 1 { 45 | return fmt.Errorf("please specify a shell (bash, powershell or zsh)") 46 | } 47 | 48 | o.shell = args[0] 49 | return nil 50 | } 51 | 52 | // Run runs completion command. 53 | func (o *completionOptions) Run(out io.Writer, rootCommand *cobra.Command) error { 54 | switch o.shell { 55 | case "bash": 56 | return rootCommand.GenBashCompletion(out) 57 | case "powershell": 58 | return rootCommand.GenPowerShellCompletion(out) 59 | case "zsh": 60 | return rootCommand.GenZshCompletion(out) 61 | default: 62 | return fmt.Errorf("invalid shell \"%s\" (supported are bash, powershell and zsh)", o.shell) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/commands/completion/completion_test.go: -------------------------------------------------------------------------------- 1 | package completion 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func Test_completionOptions_Validate(t *testing.T) { 12 | type args struct { 13 | args []string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | wantErr bool 19 | wantShell string 20 | }{ 21 | { 22 | name: "error if no args", 23 | args: args{ 24 | args: []string{}, 25 | }, 26 | wantErr: true, 27 | wantShell: "", 28 | }, 29 | { 30 | name: "error if too many args", 31 | args: args{ 32 | args: []string{"zsh", "bash"}, 33 | }, 34 | wantErr: true, 35 | wantShell: "", 36 | }, 37 | { 38 | name: "shell set to first arg", 39 | args: args{ 40 | args: []string{"zsh"}, 41 | }, 42 | wantErr: false, 43 | wantShell: "zsh", 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | o := &completionOptions{} 49 | if err := o.Validate(tt.args.args); (err != nil) != tt.wantErr { 50 | t.Errorf("completionOptions.Validate() error = %v, wantErr %v", err, tt.wantErr) 51 | return 52 | } 53 | if o.shell != tt.wantShell { 54 | t.Errorf("completionOptions.shell = %v, want %v", o.shell, tt.wantShell) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func Test_completionOptions_Run(t *testing.T) { 61 | cmd := &cobra.Command{ 62 | Use: "kyml", 63 | } 64 | 65 | type args struct { 66 | rootCommand *cobra.Command 67 | } 68 | tests := []struct { 69 | name string 70 | o *completionOptions 71 | args args 72 | wantOutToContain string 73 | wantErr bool 74 | }{ 75 | { 76 | name: "bash", 77 | o: &completionOptions{"bash"}, 78 | args: args{cmd}, 79 | wantOutToContain: "# bash completion for kyml", 80 | wantErr: false, 81 | }, 82 | { 83 | name: "powershell", 84 | o: &completionOptions{"powershell"}, 85 | args: args{cmd}, 86 | wantOutToContain: "Register-ArgumentCompleter -CommandName 'kyml'", 87 | wantErr: false, 88 | }, 89 | { 90 | name: "zsh", 91 | o: &completionOptions{"zsh"}, 92 | args: args{cmd}, 93 | wantOutToContain: "#compdef _kyml kyml", 94 | wantErr: false, 95 | }, 96 | { 97 | name: "invalid shell", 98 | o: &completionOptions{"invalid"}, 99 | args: args{cmd}, 100 | wantOutToContain: "", 101 | wantErr: true, 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | out := &bytes.Buffer{} 107 | if err := tt.o.Run(out, tt.args.rootCommand); (err != nil) != tt.wantErr { 108 | t.Errorf("completionOptions.Run() error = %v, wantErr %v", err, tt.wantErr) 109 | return 110 | } 111 | if gotOut := out.String(); !strings.Contains(gotOut, tt.wantOutToContain) { 112 | t.Errorf("completionOptions.Run() = %v, want it to contain %v", gotOut, tt.wantOutToContain) 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pkg/commands/resolve/resolve.go: -------------------------------------------------------------------------------- 1 | package resolve 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/frigus02/kyml/pkg/cat" 8 | "github.com/frigus02/kyml/pkg/k8syaml" 9 | "github.com/frigus02/kyml/pkg/resolve" 10 | "github.com/spf13/cobra" 11 | 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | ) 14 | 15 | type resolveOptions struct{} 16 | 17 | // NewCmdResolve creates a new resolve command. 18 | func NewCmdResolve(in io.Reader, out io.Writer) *cobra.Command { 19 | var o resolveOptions 20 | 21 | cmd := &cobra.Command{ 22 | Use: "resolve", 23 | Short: "Resolve image tags to their distribution digest", 24 | Long: `Resolve image tags in Kubernetes YAML documents to their distribution digest. Data is read from stdin and printed to stdout. 25 | 26 | This can be helpful if you tag the same image multiple times, e.g. because you build for every commit and use the commit sha as the Docker tag. Resolving the tag to the content digest before sending the manifests to Kubernetes makes sure your services only restart if the image actually changed. 27 | 28 | In case an image is multi platform, it is resolved to the linux amd64 variant.`, 29 | Example: ` # Resolve image tags before deploying to cluster 30 | kyml cat feature/* | kyml resolve | kubectl apply -f -`, 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | err := o.Validate(args) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return o.Run(in, out, resolveImage) 38 | }, 39 | } 40 | 41 | return cmd 42 | } 43 | 44 | // Validate validates resolve command. 45 | func (o *resolveOptions) Validate(args []string) error { 46 | if len(args) != 0 { 47 | return fmt.Errorf("this command takes no positional arguments") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // Run runs resolve command. 54 | func (o *resolveOptions) Run(in io.Reader, out io.Writer, resolveImage imageResolver) error { 55 | documents, err := cat.StreamDecodeOnly(in) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | resolvedImageMap := make(map[string]string) 61 | for _, doc := range documents { 62 | if pathToPodSpec := getPathToPodSpec(doc.GroupVersionKind()); pathToPodSpec != nil { 63 | obj := doc.UnstructuredContent() 64 | 65 | pathToInitContainers := append(pathToPodSpec, "initContainers") 66 | if err := resolveImagesInContainers(obj, resolveImage, resolvedImageMap, pathToInitContainers...); err != nil { 67 | return err 68 | } 69 | 70 | pathToContainers := append(pathToPodSpec, "containers") 71 | if err := resolveImagesInContainers(obj, resolveImage, resolvedImageMap, pathToContainers...); err != nil { 72 | return err 73 | } 74 | } 75 | } 76 | 77 | return k8syaml.Encode(out, documents) 78 | } 79 | 80 | type imageResolver func(imageRef string) (resolveImage string, err error) 81 | 82 | func resolveImage(imageRef string) (string, error) { 83 | return resolve.Resolve(imageRef) 84 | } 85 | 86 | func resolveImagesInContainers( 87 | obj map[string]interface{}, 88 | resolveImage imageResolver, 89 | resolvedImageMap map[string]string, 90 | fields ...string, 91 | ) error { 92 | containers, found, err := unstructured.NestedSlice(obj, fields...) 93 | if !found || err != nil { 94 | return nil 95 | } 96 | 97 | for _, container := range containers { 98 | container, ok := container.(map[string]interface{}) 99 | if !ok { 100 | return nil 101 | } 102 | 103 | image, ok := container["image"].(string) 104 | if !ok { 105 | return nil 106 | } 107 | 108 | resolvedImage, ok := resolvedImageMap[image] 109 | if !ok { 110 | resolvedImage, err = resolveImage(image) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | if resolvedImage == "" { 116 | return fmt.Errorf("image %s not found", image) 117 | } 118 | 119 | resolvedImageMap[image] = resolvedImage 120 | } 121 | 122 | container["image"] = resolvedImage 123 | } 124 | 125 | return unstructured.SetNestedSlice(obj, containers, fields...) 126 | } 127 | -------------------------------------------------------------------------------- /pkg/commands/resolve/resolve_test.go: -------------------------------------------------------------------------------- 1 | package resolve 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var testManifestService = `--- 13 | apiVersion: v1 14 | kind: Service 15 | metadata: 16 | name: the-service 17 | spec: 18 | ports: 19 | - port: 80 20 | protocol: TCP 21 | selector: 22 | deployment: hello 23 | type: LoadBalancer 24 | ` 25 | 26 | var testManifestDeploymentNoContainers = `--- 27 | apiVersion: apps/v1 28 | kind: Deployment 29 | metadata: 30 | name: the-deployment 31 | spec: 32 | template: 33 | spec: 34 | serviceAccountName: john 35 | ` 36 | 37 | var testManifestDeploymentMalformedContainer = `--- 38 | apiVersion: apps/v1 39 | kind: Deployment 40 | metadata: 41 | name: the-deployment 42 | spec: 43 | template: 44 | spec: 45 | containers: 46 | - this should not be a string 47 | ` 48 | 49 | var testManifestDeploymentNoImage = `--- 50 | apiVersion: apps/v1 51 | kind: Deployment 52 | metadata: 53 | name: the-deployment 54 | spec: 55 | template: 56 | spec: 57 | containers: 58 | - name: the-container 59 | ` 60 | 61 | var testManifestDeployment = `--- 62 | apiVersion: apps/v1 63 | kind: Deployment 64 | metadata: 65 | name: deployment-a 66 | spec: 67 | template: 68 | spec: 69 | containers: 70 | - image: kyml/hello 71 | name: the-container 72 | initContainers: 73 | - image: kyml/init 74 | name: the-init-container 75 | --- 76 | apiVersion: apps/v1 77 | kind: Deployment 78 | metadata: 79 | name: deployment-b 80 | spec: 81 | template: 82 | spec: 83 | containers: 84 | - image: kyml/hello 85 | name: the-container 86 | ` 87 | 88 | var testManifestDeploymentResolved = `--- 89 | apiVersion: apps/v1 90 | kind: Deployment 91 | metadata: 92 | name: deployment-a 93 | spec: 94 | template: 95 | spec: 96 | containers: 97 | - image: kyml/hello@sha256:2cbb95c7479634c53bc2be243554a98d6928c189360fa958d2c970974e7f131f 98 | name: the-container 99 | initContainers: 100 | - image: kyml/init@sha256:2cbb95c7479634c53bc2be243554a98d6928c189360fa958d2c970974e7f131f 101 | name: the-init-container 102 | --- 103 | apiVersion: apps/v1 104 | kind: Deployment 105 | metadata: 106 | name: deployment-b 107 | spec: 108 | template: 109 | spec: 110 | containers: 111 | - image: kyml/hello@sha256:2cbb95c7479634c53bc2be243554a98d6928c189360fa958d2c970974e7f131f 112 | name: the-container 113 | ` 114 | 115 | func Test_resolveOptions_Validate(t *testing.T) { 116 | type args struct { 117 | args []string 118 | } 119 | tests := []struct { 120 | name string 121 | args args 122 | wantErr bool 123 | }{ 124 | { 125 | name: "error if any args", 126 | args: args{ 127 | args: []string{"foo"}, 128 | }, 129 | wantErr: true, 130 | }, 131 | { 132 | name: "success if no args", 133 | args: args{ 134 | args: []string{}, 135 | }, 136 | wantErr: false, 137 | }, 138 | } 139 | for _, tt := range tests { 140 | t.Run(tt.name, func(t *testing.T) { 141 | o := &resolveOptions{} 142 | if err := o.Validate(tt.args.args); (err != nil) != tt.wantErr { 143 | t.Errorf("resolveOptions.Validate() error = %v, wantErr %v", err, tt.wantErr) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func Test_resolveOptions_Run(t *testing.T) { 150 | type args struct { 151 | in io.Reader 152 | } 153 | tests := []struct { 154 | name string 155 | args args 156 | resolveOut map[string]string 157 | resolveErr error 158 | wantOut string 159 | wantResolveCount int 160 | wantImageRefs []string 161 | wantErr bool 162 | }{ 163 | { 164 | name: "skip unsupported kinds", 165 | args: args{strings.NewReader(testManifestService)}, 166 | resolveOut: nil, 167 | resolveErr: nil, 168 | wantOut: testManifestService, 169 | wantResolveCount: 0, 170 | wantImageRefs: nil, 171 | wantErr: false, 172 | }, 173 | { 174 | name: "containers array does not exist", 175 | args: args{strings.NewReader(testManifestDeploymentNoContainers)}, 176 | resolveOut: nil, 177 | resolveErr: nil, 178 | wantOut: testManifestDeploymentNoContainers, 179 | wantResolveCount: 0, 180 | wantImageRefs: nil, 181 | wantErr: false, 182 | }, 183 | { 184 | name: "container is no yaml object", 185 | args: args{strings.NewReader(testManifestDeploymentMalformedContainer)}, 186 | resolveOut: nil, 187 | resolveErr: nil, 188 | wantOut: testManifestDeploymentMalformedContainer, 189 | wantResolveCount: 0, 190 | wantImageRefs: nil, 191 | wantErr: false, 192 | }, 193 | { 194 | name: "container has no image", 195 | args: args{strings.NewReader(testManifestDeploymentNoImage)}, 196 | resolveOut: nil, 197 | resolveErr: nil, 198 | wantOut: testManifestDeploymentNoImage, 199 | wantResolveCount: 0, 200 | wantImageRefs: nil, 201 | wantErr: false, 202 | }, 203 | { 204 | name: "resolve errors", 205 | args: args{strings.NewReader(testManifestDeployment)}, 206 | resolveOut: nil, 207 | resolveErr: errors.New("oh no"), 208 | wantOut: "", 209 | wantResolveCount: 1, 210 | wantImageRefs: []string{"kyml/init"}, 211 | wantErr: true, 212 | }, 213 | { 214 | name: "resolve doesn't find image", 215 | args: args{strings.NewReader(testManifestDeployment)}, 216 | resolveOut: nil, 217 | resolveErr: nil, 218 | wantOut: "", 219 | wantResolveCount: 1, 220 | wantImageRefs: []string{"kyml/init"}, 221 | wantErr: true, 222 | }, 223 | { 224 | name: "image gets resolved", 225 | args: args{strings.NewReader(testManifestDeployment)}, 226 | resolveOut: map[string]string{ 227 | "kyml/init": "kyml/init@sha256:2cbb95c7479634c53bc2be243554a98d6928c189360fa958d2c970974e7f131f", 228 | "kyml/hello": "kyml/hello@sha256:2cbb95c7479634c53bc2be243554a98d6928c189360fa958d2c970974e7f131f", 229 | }, 230 | resolveErr: nil, 231 | wantOut: testManifestDeploymentResolved, 232 | wantResolveCount: 2, 233 | wantImageRefs: []string{"kyml/init", "kyml/hello"}, 234 | wantErr: false, 235 | }, 236 | } 237 | for _, tt := range tests { 238 | t.Run(tt.name, func(t *testing.T) { 239 | o := &resolveOptions{} 240 | out := &bytes.Buffer{} 241 | gotResolveCount := 0 242 | var gotImageRefs []string 243 | resolveImageMock := func(imageRef string) (string, error) { 244 | gotResolveCount++ 245 | gotImageRefs = append(gotImageRefs, imageRef) 246 | if tt.resolveErr != nil { 247 | return "", tt.resolveErr 248 | } 249 | 250 | return tt.resolveOut[imageRef], nil 251 | } 252 | 253 | if err := o.Run(tt.args.in, out, resolveImageMock); (err != nil) != tt.wantErr { 254 | t.Errorf("resolveOptions.Run() error = %v, wantErr %v", err, tt.wantErr) 255 | return 256 | } 257 | if !reflect.DeepEqual(gotResolveCount, tt.wantResolveCount) { 258 | t.Errorf("resolveOptions.Run() resolveCount = %v, wantResolveCount %v", gotResolveCount, tt.wantResolveCount) 259 | return 260 | } 261 | if !reflect.DeepEqual(gotImageRefs, tt.wantImageRefs) { 262 | t.Errorf("resolveOptions.Run() imageRefs = %v, wantImageRefs %v", gotImageRefs, tt.wantImageRefs) 263 | return 264 | } 265 | if gotOut := out.String(); gotOut != tt.wantOut { 266 | t.Errorf("resolveOptions.Run() = %v, want %v", gotOut, tt.wantOut) 267 | } 268 | }) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /pkg/commands/resolve/supported_kinds.go: -------------------------------------------------------------------------------- 1 | package resolve 2 | 3 | import ( 4 | "github.com/frigus02/kyml/pkg/k8syaml" 5 | 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | // We only want to resolve images mentioned in the `image` property of 10 | // containers. These currently only appear in PodSpec, which is under the path 11 | // spec.template.spec in the listed resource kinds. 12 | // 13 | // See: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#container-v1-core 14 | var supportedKinds = []struct { 15 | GroupVersionKind schema.GroupVersionKind 16 | PathToPodSpec []string 17 | }{ 18 | { 19 | GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "DaemonSet"}, 20 | PathToPodSpec: []string{"spec", "template", "spec"}, 21 | }, 22 | { 23 | GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 24 | PathToPodSpec: []string{"spec", "template", "spec"}, 25 | }, 26 | { 27 | GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}, 28 | PathToPodSpec: []string{"spec", "template", "spec"}, 29 | }, 30 | { 31 | GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}, 32 | PathToPodSpec: []string{"spec", "template", "spec"}, 33 | }, 34 | { 35 | GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}, 36 | PathToPodSpec: []string{"spec", "template", "spec"}, 37 | }, 38 | { 39 | GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1beta1", Kind: "CronJob"}, 40 | PathToPodSpec: []string{"spec", "jobTemplate", "spec", "template", "spec"}, 41 | }, 42 | { 43 | GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ReplicationController"}, 44 | PathToPodSpec: []string{"spec", "template", "spec"}, 45 | }, 46 | } 47 | 48 | func getPathToPodSpec(gvk schema.GroupVersionKind) []string { 49 | for _, kind := range supportedKinds { 50 | if k8syaml.GVKEquals(gvk, kind.GroupVersionKind) { 51 | return kind.PathToPodSpec 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/commands/resolve/supported_kinds_test.go: -------------------------------------------------------------------------------- 1 | package resolve 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | func Test_getPathToPodSpec(t *testing.T) { 11 | type args struct { 12 | gvk schema.GroupVersionKind 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want []string 18 | }{ 19 | { 20 | name: "supported", 21 | args: args{schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}}, 22 | want: []string{"spec", "template", "spec"}, 23 | }, 24 | { 25 | name: "not supported", 26 | args: args{schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}}, 27 | want: nil, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | if got := getPathToPodSpec(tt.args.gvk); !reflect.DeepEqual(got, tt.want) { 33 | t.Errorf("getPathToPodSpec() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/commands/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/frigus02/kyml/pkg/cat" 10 | "github.com/frigus02/kyml/pkg/diff" 11 | "github.com/frigus02/kyml/pkg/fs" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type testOptions struct { 16 | files []string 17 | nameComparison string 18 | nameMain string 19 | snapshotFile string 20 | updateSnapshot bool 21 | } 22 | 23 | // NewCmdTest creates a new test command. 24 | func NewCmdTest(in io.Reader, out io.Writer, fs fs.Filesystem) *cobra.Command { 25 | var o testOptions 26 | 27 | cmd := &cobra.Command{ 28 | Use: "test ...", 29 | Short: "Run a snapshot test on the diff between Kubernetes YAML files of two environments", 30 | Long: `Run a snapshot test on the diff between Kubernetes YAML files of two environments. 31 | 32 | Integrate this in your CI builds to make sure your environments don't accidentially drift apart. 33 | 34 | The main environment is specified on stdin. Use "kyml cat" to concatenate multiple files and pipe the result into "kyml test". 35 | 36 | The comparison environment is specified using filenames. Files are concatenated using the same rules as in "kyml cat". 37 | 38 | The command compares the diff between these environments to a previous diff stored in the specified snapshot file. If it matches, it prints the main environment to stdout, so it can be piped into followup commands like "kyml tmpl" or "kubectl apply". If it doesn't match, it prints the diff to stderr and exits with a non-zero exit code.`, 39 | Example: ` # Make sure production and staging don't drift apart unknowingly 40 | kyml cat production/* | kyml test staging/* \ 41 | --name-main production \ 42 | --name-staging staging \ 43 | --snapshot-file tests/prod-vs-staging.diff 44 | 45 | # Update snapshot file when the change was deliberate 46 | kyml cat production/* | kyml test staging/* \ 47 | --name-main production \ 48 | --name-staging staging \ 49 | --snapshot-file tests/prod-vs-staging.diff \ 50 | --update`, 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | err := o.Validate(args) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return o.Run(in, out, fs) 58 | }, 59 | } 60 | 61 | cmd.Flags().StringVar(&o.nameMain, "name-main", "main", "Name of the main environment read from stdin") 62 | cmd.Flags().StringVar(&o.nameComparison, "name-comparison", "comparison", "Name of the comparison environment read from files") 63 | cmd.Flags().StringVarP(&o.snapshotFile, "snapshot-file", "s", "kyml-snapshot.diff", "Snapshot file") 64 | cmd.Flags().BoolVarP(&o.updateSnapshot, "update", "u", false, "If specified, update snapshot file and exit successfully in case of non-match") 65 | 66 | _ = cmd.MarkFlagFilename("snapshot-file") 67 | 68 | // Test supports infinite positional file arguments, however zsh completions 69 | // require each positional argument to be marked individually. We just mark 70 | // the first few. 71 | _ = cmd.MarkZshCompPositionalArgumentFile(1) 72 | _ = cmd.MarkZshCompPositionalArgumentFile(2) 73 | _ = cmd.MarkZshCompPositionalArgumentFile(3) 74 | _ = cmd.MarkZshCompPositionalArgumentFile(4) 75 | _ = cmd.MarkZshCompPositionalArgumentFile(5) 76 | 77 | return cmd 78 | } 79 | 80 | // Validate validates test command. 81 | func (o *testOptions) Validate(args []string) error { 82 | if len(args) == 0 { 83 | return fmt.Errorf("specify at least one file for the comparison environment") 84 | } 85 | 86 | o.files = args 87 | return nil 88 | } 89 | 90 | // Run runs test command. 91 | func (o *testOptions) Run(in io.Reader, out io.Writer, fs fs.Filesystem) error { 92 | var bufferMain bytes.Buffer 93 | if err := cat.Stream(&bufferMain, in); err != nil { 94 | return err 95 | } 96 | 97 | var bufferComparison bytes.Buffer 98 | if err := cat.Cat(&bufferComparison, o.files, fs); err != nil { 99 | return err 100 | } 101 | 102 | diffStr, err := diff.Diff( 103 | o.nameMain, bufferMain.String(), 104 | o.nameComparison, bufferComparison.String()) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | snapshotBytes, err := fs.ReadFile(o.snapshotFile) 110 | if err != nil { 111 | if !os.IsNotExist(err) { 112 | return fmt.Errorf("cannot open snapshot file: %v", err) 113 | } 114 | 115 | if !o.updateSnapshot { 116 | return fmt.Errorf("snapshot file does not exist\nRun the command with --update to create it") 117 | } 118 | } 119 | 120 | snapshotDiffStr, err := diff.Diff( 121 | "snapshot diff", string(snapshotBytes), 122 | "this diff", diffStr) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | if snapshotDiffStr != "" { 128 | if o.updateSnapshot { 129 | if err := fs.WriteFile(o.snapshotFile, []byte(diffStr), 0644); err != nil { 130 | return err 131 | } 132 | } else { 133 | return fmt.Errorf("snapshot diff does not match this diff\n\nRun the command with --update to update it\n\n%s", snapshotDiffStr) 134 | } 135 | } 136 | 137 | fmt.Fprint(out, bufferMain.String()) 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/commands/test/test_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/frigus02/kyml/pkg/fs" 12 | ) 13 | 14 | func mustCreateFs(t *testing.T) fs.Filesystem { 15 | fsWithTestdata, err := fs.NewFakeFilesystemFromDisk( 16 | "testdata/production/deployment.yaml", 17 | "testdata/production/service.yaml", 18 | "testdata/staging/deployment.yaml", 19 | "testdata/staging/service.yaml", 20 | ) 21 | if err != nil { 22 | t.Fatalf("error reading testdata: %v", err) 23 | } 24 | 25 | return fsWithTestdata 26 | } 27 | 28 | func mustCreateFsWithSnapshot(t *testing.T, snapshot string) fs.Filesystem { 29 | fsWithTestdata := mustCreateFs(t) 30 | err := fsWithTestdata.WriteFile("kyml-snapshot.diff", []byte(snapshot), 0644) 31 | if err != nil { 32 | t.Fatalf("error writing snapshot: %v", err) 33 | } 34 | 35 | return fsWithTestdata 36 | } 37 | 38 | func mustCreateStream(t *testing.T, files ...string) io.Reader { 39 | var content []byte 40 | for _, file := range files { 41 | data, err := ioutil.ReadFile(file) 42 | if err != nil { 43 | t.Fatalf("error reading testdata: %v", err) 44 | } 45 | 46 | content = append(content, []byte("\n---\n")...) 47 | content = append(content, data...) 48 | } 49 | 50 | return bytes.NewReader(content) 51 | } 52 | 53 | func readFileOrEmpty(filename string, fs fs.Filesystem) string { 54 | data, err := fs.ReadFile(filename) 55 | if err != nil { 56 | return "" 57 | } 58 | 59 | return string(data) 60 | } 61 | 62 | func Test_testOptions_Validate(t *testing.T) { 63 | type args struct { 64 | args []string 65 | } 66 | tests := []struct { 67 | name string 68 | args args 69 | wantErr bool 70 | wantFiles []string 71 | }{ 72 | { 73 | name: "error if no args", 74 | args: args{ 75 | args: []string{}, 76 | }, 77 | wantErr: true, 78 | wantFiles: nil, 79 | }, 80 | { 81 | name: "files set to args", 82 | args: args{ 83 | args: []string{"foo", "bar", "baz"}, 84 | }, 85 | wantErr: false, 86 | wantFiles: []string{"foo", "bar", "baz"}, 87 | }, 88 | } 89 | for _, tt := range tests { 90 | o := &testOptions{} 91 | t.Run(tt.name, func(t *testing.T) { 92 | if err := o.Validate(tt.args.args); (err != nil) != tt.wantErr { 93 | t.Errorf("testOptions.Validate() error = %v, wantErr %v", err, tt.wantErr) 94 | return 95 | } 96 | if !reflect.DeepEqual(o.files, tt.wantFiles) { 97 | t.Errorf("catOptions.files = %v, want %v", o.files, tt.wantFiles) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func Test_testOptions_Run(t *testing.T) { 104 | type args struct { 105 | in io.Reader 106 | fs fs.Filesystem 107 | } 108 | tests := []struct { 109 | name string 110 | o *testOptions 111 | args args 112 | wantOut string 113 | wantSnapshot string 114 | wantErr bool 115 | wantErrToContain string 116 | }{ 117 | { 118 | name: "input files don't exist", 119 | o: &testOptions{ 120 | nameMain: "staging", 121 | nameComparison: "production", 122 | files: []string{"testdata/production/dep.yaml"}, 123 | snapshotFile: "kyml-snapshot.diff", 124 | updateSnapshot: false, 125 | }, 126 | args: args{ 127 | in: mustCreateStream(t, "testdata/staging/deployment.yaml"), 128 | fs: mustCreateFs(t), 129 | }, 130 | wantOut: "", 131 | wantSnapshot: "", 132 | wantErr: true, 133 | }, 134 | { 135 | name: "snapshot file doesn't exist", 136 | o: &testOptions{ 137 | nameMain: "staging", 138 | nameComparison: "production", 139 | files: []string{"testdata/production/deployment.yaml"}, 140 | snapshotFile: "kyml-snapshot.diff", 141 | updateSnapshot: false, 142 | }, 143 | args: args{ 144 | in: mustCreateStream(t, "testdata/staging/deployment.yaml"), 145 | fs: mustCreateFs(t), 146 | }, 147 | wantOut: "", 148 | wantSnapshot: "", 149 | wantErr: true, 150 | }, 151 | { 152 | name: "snapshot diff doesn't match", 153 | o: &testOptions{ 154 | nameMain: "staging", 155 | nameComparison: "production", 156 | files: []string{"testdata/production/deployment.yaml", "testdata/production/service.yaml"}, 157 | snapshotFile: "kyml-snapshot.diff", 158 | updateSnapshot: false, 159 | }, 160 | args: args{ 161 | in: mustCreateStream(t, "testdata/staging/deployment.yaml", "testdata/staging/service.yaml"), 162 | fs: mustCreateFsWithSnapshot(t, "--- staging\n+++ production\n@@ -19 +19 @@\n- replicas: 1\n+ replicas: 2\n"), 163 | }, 164 | wantOut: "", 165 | wantSnapshot: "--- staging\n+++ production\n@@ -19 +19 @@\n- replicas: 1\n+ replicas: 2\n", 166 | wantErr: true, 167 | wantErrToContain: "--- snapshot diff\n+++ this diff\n@@ -5 +5 @@\n-+ replicas: 2\n++ replicas: 3\n", 168 | }, 169 | { 170 | name: "snapshot diff doesn't match and update requested", 171 | o: &testOptions{ 172 | nameMain: "staging", 173 | nameComparison: "production", 174 | files: []string{"testdata/production/deployment.yaml", "testdata/production/service.yaml"}, 175 | snapshotFile: "kyml-snapshot.diff", 176 | updateSnapshot: true, 177 | }, 178 | args: args{ 179 | in: mustCreateStream(t, "testdata/staging/deployment.yaml", "testdata/staging/service.yaml"), 180 | fs: mustCreateFsWithSnapshot(t, "--- staging\n+++ production\n@@ -19 +19 @@\n- replicas: 1\n+ replicas: 2\n"), 181 | }, 182 | wantOut: "---\napiVersion: v1\nkind: Service\nmetadata:\n name: the-service\nspec:\n ports:\n - port: 80\n protocol: TCP\n selector:\n deployment: hello\n type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: the-deployment\nspec:\n replicas: 1\n template:\n spec:\n containers:\n - image: kyml/hello\n name: the-container\n", 183 | wantSnapshot: "--- staging\n+++ production\n@@ -19 +19 @@\n- replicas: 1\n+ replicas: 3\n", 184 | wantErr: false, 185 | }, 186 | { 187 | name: "snapshot diff matches", 188 | o: &testOptions{ 189 | nameMain: "staging", 190 | nameComparison: "production", 191 | files: []string{"testdata/production/deployment.yaml", "testdata/production/service.yaml"}, 192 | snapshotFile: "kyml-snapshot.diff", 193 | updateSnapshot: false, 194 | }, 195 | args: args{ 196 | in: mustCreateStream(t, "testdata/staging/deployment.yaml", "testdata/staging/service.yaml"), 197 | fs: mustCreateFsWithSnapshot(t, "--- staging\n+++ production\n@@ -19 +19 @@\n- replicas: 1\n+ replicas: 3\n"), 198 | }, 199 | wantOut: "---\napiVersion: v1\nkind: Service\nmetadata:\n name: the-service\nspec:\n ports:\n - port: 80\n protocol: TCP\n selector:\n deployment: hello\n type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: the-deployment\nspec:\n replicas: 1\n template:\n spec:\n containers:\n - image: kyml/hello\n name: the-container\n", 200 | wantSnapshot: "--- staging\n+++ production\n@@ -19 +19 @@\n- replicas: 1\n+ replicas: 3\n", 201 | wantErr: false, 202 | }, 203 | } 204 | for _, tt := range tests { 205 | t.Run(tt.name, func(t *testing.T) { 206 | out := &bytes.Buffer{} 207 | err := tt.o.Run(tt.args.in, out, tt.args.fs) 208 | if (err != nil) != tt.wantErr { 209 | t.Errorf("testOptions.Run() error = %v, wantErr %v", err, tt.wantErr) 210 | return 211 | } 212 | if err != nil && !strings.Contains(err.Error(), tt.wantErrToContain) { 213 | t.Errorf("testOptions.Run() error = %v, wantErrToContain %v", err, tt.wantErrToContain) 214 | return 215 | } 216 | if gotOut := out.String(); gotOut != tt.wantOut { 217 | t.Errorf("testOptions.Run() = %v, want %v", gotOut, tt.wantOut) 218 | return 219 | } 220 | if gotSnapshot := readFileOrEmpty(tt.o.snapshotFile, tt.args.fs); gotSnapshot != tt.wantSnapshot { 221 | t.Errorf("testOptions.Run() snapshot = %v, wantSnapshot %v", gotSnapshot, tt.wantSnapshot) 222 | } 223 | }) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /pkg/commands/test/testdata/production/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: the-deployment 5 | spec: 6 | replicas: 3 7 | template: 8 | spec: 9 | containers: 10 | - name: the-container 11 | image: kyml/hello 12 | -------------------------------------------------------------------------------- /pkg/commands/test/testdata/production/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service 5 | spec: 6 | selector: 7 | deployment: hello 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | -------------------------------------------------------------------------------- /pkg/commands/test/testdata/staging/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: the-deployment 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: the-container 11 | image: kyml/hello 12 | -------------------------------------------------------------------------------- /pkg/commands/test/testdata/staging/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service 5 | spec: 6 | selector: 7 | deployment: hello 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | -------------------------------------------------------------------------------- /pkg/commands/tmpl/tmpl.go: -------------------------------------------------------------------------------- 1 | package tmpl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "text/template" 9 | 10 | "github.com/frigus02/kyml/pkg/cat" 11 | "github.com/frigus02/kyml/pkg/k8syaml" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type tmplOptions struct { 16 | values map[string]string 17 | envVars []string 18 | } 19 | 20 | // NewCmdTmpl creates a new tmpl command. 21 | func NewCmdTmpl(in io.Reader, out io.Writer) *cobra.Command { 22 | var o tmplOptions 23 | 24 | cmd := &cobra.Command{ 25 | Use: "tmpl", 26 | Short: "Template Kubernetes YAML files", 27 | Long: `Template Kubernetes YAML files. Data is read from stdin, executed with the specified context, and printed to stdout. 28 | 29 | Templates are only supported in values of type string. They use the go template syntax (https://golang.org/pkg/text/template/). You can add data to the template context using the options "--value" and "--env". Please note that keys (including environment variable names) are case sensitive. 30 | 31 | The command parses the data as Kubernetes YAML documents before templating. While doing so it applies the same transformations as "kyml cat". Since the document is parsed, you need to make sure it is still valid YAML, even with the template characters inside.`, 32 | Example: ` # Template feature branch files and deploy to cluster 33 | kyml cat feature/* | 34 | kyml tmpl \ 35 | -v ImageTag=$(git rev-parse --short HEAD) \ 36 | -e TRAVIS_BRANCH | 37 | kubectl apply -f -`, 38 | RunE: func(cmd *cobra.Command, args []string) error { 39 | err := o.Validate(args) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return o.Run(in, out) 45 | }, 46 | } 47 | 48 | cmd.Flags().StringToStringVarP(&o.values, "value", "v", nil, "Add a key-value pair to the template context") 49 | cmd.Flags().StringArrayVarP(&o.envVars, "env", "e", nil, "Add an environment variable to the template context") 50 | 51 | return cmd 52 | } 53 | 54 | // Validate validates tmpl command. 55 | func (o *tmplOptions) Validate(args []string) error { 56 | if len(args) != 0 { 57 | return fmt.Errorf("this command takes no positional arguments") 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // Run runs tmpl command. 64 | func (o *tmplOptions) Run(in io.Reader, out io.Writer) error { 65 | vars := make(map[string]string) 66 | for key, value := range o.values { 67 | vars[key] = value 68 | } 69 | for _, env := range o.envVars { 70 | vars[env] = os.Getenv(env) 71 | } 72 | 73 | documents, err := cat.StreamDecodeOnly(in) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | var execTmpl = func(text, name string) (string, error) { 79 | tmpl, err := template.New(name).Option("missingkey=error").Parse(text) 80 | if err != nil { 81 | return "", err 82 | } 83 | 84 | var result bytes.Buffer 85 | if err = tmpl.Execute(&result, vars); err != nil { 86 | return "", err 87 | } 88 | 89 | return result.String(), nil 90 | } 91 | 92 | for _, doc := range documents { 93 | templated, err := templateValuesInMap(doc.UnstructuredContent(), execTmpl) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | doc.SetUnstructuredContent(templated) 99 | } 100 | 101 | return k8syaml.Encode(out, documents) 102 | } 103 | 104 | type valueTemplater func(text, name string) (string, error) 105 | 106 | func templateValuesInMap(m map[string]interface{}, execTmpl valueTemplater) (map[string]interface{}, error) { 107 | newMap := make(map[string]interface{}, len(m)) 108 | for key, value := range m { 109 | templated, err := templateValue(value, key, execTmpl) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | newMap[key] = templated 115 | } 116 | 117 | return newMap, nil 118 | } 119 | 120 | func templateValuesInSlice(s []interface{}, name string, execTmpl valueTemplater) ([]interface{}, error) { 121 | newSlice := make([]interface{}, len(s)) 122 | for index, value := range s { 123 | templated, err := templateValue(value, fmt.Sprintf("%s[%d]", name, index), execTmpl) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | newSlice[index] = templated 129 | } 130 | 131 | return newSlice, nil 132 | } 133 | 134 | func templateValue(value interface{}, name string, execTmpl valueTemplater) (interface{}, error) { 135 | switch value := value.(type) { 136 | case map[string]interface{}: 137 | return templateValuesInMap(value, execTmpl) 138 | case []interface{}: 139 | return templateValuesInSlice(value, name, execTmpl) 140 | case string: 141 | return execTmpl(value, name) 142 | default: 143 | return value, nil 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /pkg/commands/tmpl/tmpl_test.go: -------------------------------------------------------------------------------- 1 | package tmpl 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var testManifestDeployment = `--- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: the-deployment 16 | labels: 17 | branch: "{{.branch}}" 18 | spec: 19 | replicas: 1 20 | template: 21 | spec: 22 | containers: 23 | - name: the-container 24 | image: kyml/hello:{{.tag}} 25 | env: 26 | - name: SECRET 27 | value: "{{.SECRET}}" 28 | ` 29 | 30 | var templatedDeployment = `--- 31 | apiVersion: apps/v1 32 | kind: Deployment 33 | metadata: 34 | labels: 35 | branch: my-feature 36 | name: the-deployment 37 | spec: 38 | replicas: 1 39 | template: 40 | spec: 41 | containers: 42 | - env: 43 | - name: SECRET 44 | value: '''123_"_' 45 | image: kyml/hello:latest 46 | name: the-container 47 | ` 48 | 49 | func Test_tmplOptions_Validate(t *testing.T) { 50 | type args struct { 51 | args []string 52 | } 53 | tests := []struct { 54 | name string 55 | args args 56 | wantErr bool 57 | }{ 58 | { 59 | name: "no args", 60 | args: args{ 61 | args: []string{}, 62 | }, 63 | wantErr: false, 64 | }, 65 | { 66 | name: "error when args are specified", 67 | args: args{ 68 | args: []string{"hello"}, 69 | }, 70 | wantErr: true, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | o := &tmplOptions{} 76 | if err := o.Validate(tt.args.args); (err != nil) != tt.wantErr { 77 | t.Errorf("tmplOptions.Validate() error = %v, wantErr %v", err, tt.wantErr) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func Test_tmplOptions_Run(t *testing.T) { 84 | if err := os.Setenv("SECRET", "'123_\"_"); err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | type args struct { 89 | in io.Reader 90 | } 91 | tests := []struct { 92 | name string 93 | o *tmplOptions 94 | args args 95 | wantOut string 96 | wantErr bool 97 | }{ 98 | { 99 | name: "success", 100 | o: &tmplOptions{ 101 | values: map[string]string{ 102 | "branch": "my-feature", 103 | "tag": "latest", 104 | }, 105 | envVars: []string{"SECRET"}, 106 | }, 107 | args: args{ 108 | in: strings.NewReader(testManifestDeployment), 109 | }, 110 | wantOut: templatedDeployment, 111 | wantErr: false, 112 | }, 113 | { 114 | name: "missing value", 115 | o: &tmplOptions{ 116 | values: map[string]string{ 117 | "tag": "latest", 118 | }, 119 | envVars: []string{"SECRET"}, 120 | }, 121 | args: args{ 122 | in: strings.NewReader(testManifestDeployment), 123 | }, 124 | wantOut: "", 125 | wantErr: true, 126 | }, 127 | { 128 | name: "invalid template", 129 | o: &tmplOptions{ 130 | values: map[string]string{ 131 | "branch": "my-feature", 132 | "tag": "latest", 133 | }, 134 | }, 135 | args: args{ 136 | in: strings.NewReader("label: {{.branch\nimage: hello:{{.tag}}\n"), 137 | }, 138 | wantOut: "", 139 | wantErr: true, 140 | }, 141 | } 142 | for _, tt := range tests { 143 | t.Run(tt.name, func(t *testing.T) { 144 | out := &bytes.Buffer{} 145 | if err := tt.o.Run(tt.args.in, out); (err != nil) != tt.wantErr { 146 | t.Errorf("tmplOptions.Run() error = %v, wantErr %v", err, tt.wantErr) 147 | return 148 | } 149 | if gotOut := out.String(); gotOut != tt.wantOut { 150 | t.Errorf("tmplOptions.Run() = %v, want %v", gotOut, tt.wantOut) 151 | } 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pkg/diff/diff.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import "github.com/pmezard/go-difflib/difflib" 4 | 5 | // Diff returns a diff between the two specified strings A and B. 6 | func Diff(nameA string, a string, nameB string, b string) (string, error) { 7 | linesA := difflib.SplitLines(a) 8 | linesB := difflib.SplitLines(b) 9 | 10 | return difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ 11 | A: linesA, 12 | FromFile: nameA, 13 | B: linesB, 14 | ToFile: nameB, 15 | Context: 0, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/diff/diff_test.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import "testing" 4 | 5 | func TestDiff(t *testing.T) { 6 | type args struct { 7 | nameA string 8 | a string 9 | nameB string 10 | b string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want string 16 | wantErr bool 17 | }{ 18 | { 19 | name: "success", 20 | args: args{ 21 | nameA: "staging", 22 | a: "1\n2\n3", 23 | nameB: "production", 24 | b: "1\na\n3", 25 | }, 26 | want: "--- staging\n+++ production\n@@ -2 +2 @@\n-2\n+a\n", 27 | wantErr: false, 28 | }, 29 | { 30 | name: "no difference", 31 | args: args{ 32 | nameA: "staging", 33 | a: "1\n2\n3", 34 | nameB: "production", 35 | b: "1\n2\n3", 36 | }, 37 | want: "", 38 | wantErr: false, 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | got, err := Diff(tt.args.nameA, tt.args.a, tt.args.nameB, tt.args.b) 44 | if (err != nil) != tt.wantErr { 45 | t.Errorf("Diff() error = %v, wantErr %v", err, tt.wantErr) 46 | return 47 | } 48 | if got != tt.want { 49 | t.Errorf("Diff() = %v, want %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/fs/fake_file.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | ) 7 | 8 | type fakeFile struct { 9 | data *bytes.Buffer 10 | isClosed bool 11 | } 12 | 13 | func (f *fakeFile) Read(p []byte) (n int, err error) { 14 | if f.isClosed { 15 | return 0, os.ErrClosed 16 | } 17 | 18 | return f.data.Read(p) 19 | } 20 | 21 | func (f *fakeFile) Close() error { 22 | if f.isClosed { 23 | return os.ErrClosed 24 | } 25 | 26 | f.isClosed = true 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/fs/fake_filesystem.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | type fakeFilesystem struct { 10 | files map[string][]byte 11 | fileModes map[string]os.FileMode 12 | } 13 | 14 | // NewFakeFilesystem creates a new filesystem using the OS. 15 | func NewFakeFilesystem() Filesystem { 16 | return &fakeFilesystem{ 17 | files: make(map[string][]byte), 18 | fileModes: make(map[string]os.FileMode), 19 | } 20 | } 21 | 22 | // NewFakeFilesystemFromDisk creates a new filesystem prefilled with the 23 | // specified files, which are read from disk. 24 | func NewFakeFilesystemFromDisk(files ...string) (Filesystem, error) { 25 | fs := NewFakeFilesystem() 26 | for _, file := range files { 27 | data, err := ioutil.ReadFile(file) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | if err = fs.WriteFile(file, data, 0644); err != nil { 33 | return nil, err 34 | } 35 | } 36 | 37 | return fs, nil 38 | } 39 | 40 | func (fs *fakeFilesystem) Open(name string) (File, error) { 41 | if data, ok := fs.files[name]; ok { 42 | dataCopy := make([]byte, len(data)) 43 | copy(dataCopy, data) 44 | return &fakeFile{bytes.NewBuffer(dataCopy), false}, nil 45 | } 46 | 47 | return nil, os.ErrNotExist 48 | } 49 | 50 | func (fs *fakeFilesystem) ReadFile(filename string) ([]byte, error) { 51 | if data, ok := fs.files[filename]; ok { 52 | dataCopy := make([]byte, len(data)) 53 | copy(dataCopy, data) 54 | return dataCopy, nil 55 | } 56 | 57 | return nil, os.ErrNotExist 58 | } 59 | 60 | func (fs *fakeFilesystem) WriteFile(filename string, data []byte, perm os.FileMode) error { 61 | fs.files[filename] = data 62 | fs.fileModes[filename] = perm 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/fs/file.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import "io" 4 | 5 | // File is an interface abstraction for `os.File`. 6 | type File interface { 7 | io.ReadCloser 8 | } 9 | -------------------------------------------------------------------------------- /pkg/fs/filesystem.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import "os" 4 | 5 | // Filesystem is an interface abstraction for some file system methods on `os` 6 | // and `ioutil`. 7 | type Filesystem interface { 8 | Open(name string) (File, error) 9 | ReadFile(filename string) ([]byte, error) 10 | WriteFile(filename string, data []byte, perm os.FileMode) error 11 | } 12 | -------------------------------------------------------------------------------- /pkg/fs/os_file.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import "os" 4 | 5 | type osFile struct { 6 | file *os.File 7 | } 8 | 9 | func (f osFile) Read(p []byte) (int, error) { 10 | return f.file.Read(p) 11 | } 12 | 13 | func (f osFile) Close() error { 14 | return f.file.Close() 15 | } 16 | -------------------------------------------------------------------------------- /pkg/fs/os_filesystem.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | type osFilesystem struct{} 9 | 10 | // NewOSFilesystem creates a new filesystem using the OS. 11 | func NewOSFilesystem() Filesystem { 12 | return osFilesystem{} 13 | } 14 | 15 | func (fs osFilesystem) Open(name string) (File, error) { 16 | file, err := os.Open(name) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return osFile{file}, nil 22 | } 23 | 24 | func (fs osFilesystem) ReadFile(filename string) ([]byte, error) { 25 | return ioutil.ReadFile(filename) 26 | } 27 | 28 | func (fs osFilesystem) WriteFile(filename string, data []byte, perm os.FileMode) error { 29 | return ioutil.WriteFile(filename, data, perm) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/k8syaml/gvk.go: -------------------------------------------------------------------------------- 1 | package k8syaml 2 | 3 | import "k8s.io/apimachinery/pkg/runtime/schema" 4 | 5 | // GVKEquals returns true if both specified struct have equal fields. 6 | func GVKEquals(a, b schema.GroupVersionKind) bool { 7 | return a.Group == b.Group && a.Version == b.Version && a.Kind == b.Kind 8 | } 9 | -------------------------------------------------------------------------------- /pkg/k8syaml/gvk_test.go: -------------------------------------------------------------------------------- 1 | package k8syaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | func TestGVKEquals(t *testing.T) { 10 | type args struct { 11 | a schema.GroupVersionKind 12 | b schema.GroupVersionKind 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want bool 18 | }{ 19 | { 20 | name: "equal", 21 | args: args{ 22 | schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 23 | schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 24 | }, 25 | want: true, 26 | }, 27 | { 28 | name: "different group", 29 | args: args{ 30 | schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 31 | schema.GroupVersionKind{Group: "extensions", Version: "v1", Kind: "Deployment"}, 32 | }, 33 | want: false, 34 | }, 35 | { 36 | name: "different version", 37 | args: args{ 38 | schema.GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "Deployment"}, 39 | schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 40 | }, 41 | want: false, 42 | }, 43 | { 44 | name: "different kind", 45 | args: args{ 46 | schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 47 | schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}, 48 | }, 49 | want: false, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if got := GVKEquals(tt.args.a, tt.args.b); got != tt.want { 55 | t.Errorf("GVKEquals() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/k8syaml/k8syaml.go: -------------------------------------------------------------------------------- 1 | package k8syaml 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | yamlUtil "k8s.io/apimachinery/pkg/util/yaml" 9 | "sigs.k8s.io/yaml" 10 | ) 11 | 12 | // Decode returns a slice of parse unstructured Kubernetes API objects from a 13 | // specified JSON or YAML file. 14 | func Decode(in io.Reader) ([]*unstructured.Unstructured, error) { 15 | decoder := yamlUtil.NewYAMLToJSONDecoder(in) 16 | var result []*unstructured.Unstructured 17 | var err error 18 | for err == nil || isEmptyYamlError(err) { 19 | var out unstructured.Unstructured 20 | err = decoder.Decode(&out) 21 | if err == nil && out.Object != nil { 22 | result = append(result, &out) 23 | } 24 | } 25 | 26 | if err != io.EOF { 27 | return nil, err 28 | } 29 | 30 | return result, nil 31 | } 32 | 33 | // Encode prints the specified documents encode as YAML into the writer. 34 | func Encode(out io.Writer, documents []*unstructured.Unstructured) error { 35 | for _, doc := range documents { 36 | bytes, err := yaml.Marshal(doc.Object) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if _, err = out.Write([]byte("---\n")); err != nil { 42 | return err 43 | } 44 | 45 | if _, err = out.Write(bytes); err != nil { 46 | return err 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func isEmptyYamlError(err error) bool { 54 | return strings.Contains(err.Error(), "is missing in 'null'") 55 | } 56 | -------------------------------------------------------------------------------- /pkg/k8syaml/k8syaml_test.go: -------------------------------------------------------------------------------- 1 | package k8syaml 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | ) 12 | 13 | var validYamlIncludingNils = ` 14 | --- 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: the-namespace 19 | --- 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | 24 | metadata: 25 | name: the-service 26 | namespace: the-namespace 27 | 28 | spec: 29 | selector: 30 | deployment: the-deployment 31 | ` 32 | 33 | var validYamlNicelyFormatted = `--- 34 | apiVersion: v1 35 | kind: Namespace 36 | metadata: 37 | name: the-namespace 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: the-service 43 | namespace: the-namespace 44 | spec: 45 | selector: 46 | deployment: the-deployment 47 | ` 48 | 49 | var invalidYaml = ` 50 | hello world!! 51 | ` 52 | 53 | var unstructuredDocuments = []*unstructured.Unstructured{ 54 | { 55 | Object: map[string]interface{}{ 56 | "apiVersion": "v1", 57 | "kind": "Namespace", 58 | "metadata": map[string]interface{}{ 59 | "name": "the-namespace", 60 | }, 61 | }, 62 | }, 63 | { 64 | Object: map[string]interface{}{ 65 | "apiVersion": "v1", 66 | "kind": "Service", 67 | "metadata": map[string]interface{}{ 68 | "name": "the-service", 69 | "namespace": "the-namespace", 70 | }, 71 | "spec": map[string]interface{}{ 72 | "selector": map[string]interface{}{ 73 | "deployment": "the-deployment", 74 | }, 75 | }, 76 | }, 77 | }, 78 | } 79 | 80 | func TestDecode(t *testing.T) { 81 | type args struct { 82 | in io.Reader 83 | } 84 | tests := []struct { 85 | name string 86 | args args 87 | want []*unstructured.Unstructured 88 | wantErr bool 89 | }{ 90 | { 91 | name: "valid yaml, including nil documents", 92 | args: args{strings.NewReader(validYamlIncludingNils)}, 93 | want: unstructuredDocuments, 94 | wantErr: false, 95 | }, 96 | { 97 | name: "invalid yaml", 98 | args: args{strings.NewReader(invalidYaml)}, 99 | want: nil, 100 | wantErr: true, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | got, err := Decode(tt.args.in) 106 | if (err != nil) != tt.wantErr { 107 | t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) 108 | return 109 | } 110 | if !reflect.DeepEqual(got, tt.want) { 111 | t.Errorf("Decode() = %#v, want %#v", got, tt.want) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestEncode(t *testing.T) { 118 | type args struct { 119 | documents []*unstructured.Unstructured 120 | } 121 | tests := []struct { 122 | name string 123 | args args 124 | wantOut string 125 | wantErr bool 126 | }{ 127 | { 128 | name: "adds --- between yaml documents", 129 | args: args{unstructuredDocuments}, 130 | wantOut: validYamlNicelyFormatted, 131 | wantErr: false, 132 | }, 133 | } 134 | for _, tt := range tests { 135 | t.Run(tt.name, func(t *testing.T) { 136 | out := &bytes.Buffer{} 137 | if err := Encode(out, tt.args.documents); (err != nil) != tt.wantErr { 138 | t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr) 139 | return 140 | } 141 | if gotOut := out.String(); gotOut != tt.wantOut { 142 | t.Errorf("Encode() = %v, want %v", gotOut, tt.wantOut) 143 | } 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pkg/resolve/resolve.go: -------------------------------------------------------------------------------- 1 | package resolve 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | // Resolve takes a Docker image reference (not an image ID) and attempts to 13 | // resolve this to the image distribution digest. It does this using after 14 | // another "docker inspect" and "docker manifest inspect". If any of these 15 | // succeed, it returns the image reference minus tag plus resolved digest. 16 | func Resolve(imageRef string) (string, error) { 17 | resolved, err := resolveWithDockerInspect(imageRef, execCmd) 18 | if resolved != "" || err != nil { 19 | return resolved, err 20 | } 21 | 22 | return resolveWithDockerManifestInspect(imageRef, execCmd) 23 | } 24 | 25 | type commandExecutor func(name string, arg ...string) (out []byte, err error) 26 | 27 | func execCmd(name string, arg ...string) ([]byte, error) { 28 | cmd := exec.Command(name, arg...) 29 | cmd.Env = os.Environ() 30 | return cmd.Output() 31 | } 32 | 33 | func resolveWithDockerInspect(imageRef string, execCmd commandExecutor) (string, error) { 34 | out, err := execCmd("docker", "inspect", "--format", "{{json .RepoDigests}}", imageRef) 35 | if err != nil { 36 | var eerr *exec.ExitError 37 | if errors.As(err, &eerr) { 38 | stderr := string(eerr.Stderr) 39 | if strings.Contains(stderr, "No such object:") { 40 | return "", nil 41 | } 42 | 43 | return "", fmt.Errorf("docker inspect: %v (stderr: %s)", err, stderr) 44 | } 45 | 46 | return "", fmt.Errorf("docker inspect: %v", err) 47 | } 48 | 49 | var repoDigests []string 50 | if err = json.Unmarshal(out, &repoDigests); err != nil { 51 | return "", fmt.Errorf("json decode repo digests: %v", err) 52 | } 53 | 54 | digestPrefix := removeTagAndDigest(imageRef) + "@" 55 | for _, digest := range repoDigests { 56 | if strings.HasPrefix(digest, digestPrefix) { 57 | return digest, nil 58 | } 59 | } 60 | 61 | return "", nil 62 | } 63 | 64 | func resolveWithDockerManifestInspect(imageRef string, execCmd commandExecutor) (string, error) { 65 | out, err := execCmd("docker", "manifest", "inspect", "--verbose", imageRef) 66 | if err != nil { 67 | var eerr *exec.ExitError 68 | if errors.As(err, &eerr) { 69 | stderr := string(eerr.Stderr) 70 | if strings.Contains(stderr, "no such manifest:") { 71 | return "", nil 72 | } 73 | 74 | if strings.Contains(stderr, "docker manifest inspect is only supported on a Docker cli with experimental cli features enabled") { 75 | return "", errors.New("Experimental Docker CLI features required.\n\nIf an image " + 76 | "doesn't exist locally, kyml resolve relies on the `docker manifest inspect` " + 77 | "command to resolve it. This is still experimental. You need to enable " + 78 | "experimental features in Docker to make it work. For more information and " + 79 | "how to enable experimental features see: https://docs.docker.com/engine/reference/commandline/manifest_inspect/") 80 | } 81 | 82 | return "", fmt.Errorf("docker manifest inspect: %v (stderr: %s)", err, stderr) 83 | } 84 | 85 | return "", fmt.Errorf("docker manifest inspect: %v", err) 86 | } 87 | 88 | var digest string 89 | 90 | if string(out)[0] == '[' { 91 | // The registry returned a multi platform manifest list. In this case 92 | // we always return linux amd64. 93 | // See: https://blog.docker.com/2017/09/docker-official-images-now-multi-platform/ 94 | // See: https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list 95 | 96 | var result []struct { 97 | Descriptor struct { 98 | Digest string `json:"digest"` 99 | Platform struct { 100 | Architecture string `json:"architecture"` 101 | OS string `json:"os"` 102 | } `json:"platform"` 103 | } `json:"Descriptor"` 104 | } 105 | if err = json.Unmarshal(out, &result); err != nil { 106 | return "", fmt.Errorf("json decode manifest list: %v", err) 107 | } 108 | 109 | for _, platformImage := range result { 110 | if platformImage.Descriptor.Platform.Architecture == "amd64" && 111 | platformImage.Descriptor.Platform.OS == "linux" { 112 | digest = platformImage.Descriptor.Digest 113 | break 114 | } 115 | } 116 | 117 | if digest == "" { 118 | return "", nil 119 | } 120 | } else { 121 | var result struct { 122 | Descriptor struct { 123 | Digest string `json:"digest"` 124 | } `json:"Descriptor"` 125 | } 126 | if err = json.Unmarshal(out, &result); err != nil { 127 | return "", fmt.Errorf("json decode manifest: %v", err) 128 | } 129 | 130 | digest = result.Descriptor.Digest 131 | } 132 | 133 | return removeTagAndDigest(imageRef) + "@" + digest, nil 134 | } 135 | 136 | func removeTagAndDigest(imageRef string) string { 137 | indexAt := strings.Index(imageRef, "@") 138 | if indexAt > -1 { 139 | imageRef = imageRef[0:indexAt] 140 | } 141 | 142 | lastIndexColon := strings.LastIndex(imageRef, ":") 143 | if lastIndexColon > strings.LastIndex(imageRef, "/") { 144 | imageRef = imageRef[0:lastIndexColon] 145 | } 146 | 147 | return imageRef 148 | } 149 | -------------------------------------------------------------------------------- /pkg/resolve/resolve_test.go: -------------------------------------------------------------------------------- 1 | package resolve 2 | 3 | import ( 4 | "errors" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func Test_resolveWithDockerInspect(t *testing.T) { 11 | type args struct { 12 | imageRef string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | cmdOut []byte 18 | cmdErr error 19 | want string 20 | wantCmd string 21 | wantErr bool 22 | }{ 23 | { 24 | name: "image not found", 25 | args: args{"registry:5000/path/hello:latest"}, 26 | cmdOut: []byte(""), 27 | cmdErr: &exec.ExitError{ 28 | Stderr: []byte("\nError: No such object: registry:5000/path/hello:latest\n"), 29 | }, 30 | want: "", 31 | wantCmd: "docker inspect --format {{json .RepoDigests}} registry:5000/path/hello:latest", 32 | wantErr: false, 33 | }, 34 | { 35 | name: "Docker daemon not running", 36 | args: args{"registry:5000/path/hello:latest"}, 37 | cmdOut: []byte(""), 38 | cmdErr: &exec.ExitError{ 39 | Stderr: []byte("\nError response from daemon: Bad response from Docker engine\n"), 40 | }, 41 | want: "", 42 | wantCmd: "docker inspect --format {{json .RepoDigests}} registry:5000/path/hello:latest", 43 | wantErr: true, 44 | }, 45 | { 46 | name: "docker command not found", 47 | args: args{"registry:5000/path/hello:latest"}, 48 | cmdOut: []byte(""), 49 | cmdErr: errors.New("docker inspect: exec: \"docker\": executable file not found in $PATH"), 50 | want: "", 51 | wantCmd: "docker inspect --format {{json .RepoDigests}} registry:5000/path/hello:latest", 52 | wantErr: true, 53 | }, 54 | { 55 | name: "success", 56 | args: args{"registry:5000/path/hello:latest"}, 57 | cmdOut: []byte("[\"another-registry.example.com/hello@sha256:2d8b22d01ca51eef988ff3ae8dcf37c182553b662ea47d3d62ce8208a3b83aef\",\"registry:5000/path/hello@sha256:2d8b22d01ca51eef988ff3ae8dcf37c182553b662ea47d3d62ce8208a3b83aef\"]\n"), 58 | cmdErr: nil, 59 | want: "registry:5000/path/hello@sha256:2d8b22d01ca51eef988ff3ae8dcf37c182553b662ea47d3d62ce8208a3b83aef", 60 | wantCmd: "docker inspect --format {{json .RepoDigests}} registry:5000/path/hello:latest", 61 | wantErr: false, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | var gotCmd string 67 | execCmdMock := func(name string, arg ...string) ([]byte, error) { 68 | gotCmd = name + " " + strings.Join(arg, " ") 69 | return tt.cmdOut, tt.cmdErr 70 | } 71 | 72 | got, err := resolveWithDockerInspect(tt.args.imageRef, execCmdMock) 73 | if (err != nil) != tt.wantErr { 74 | t.Errorf("resolveWithDockerInspect() error = %v, wantErr %v", err, tt.wantErr) 75 | return 76 | } 77 | if gotCmd != tt.wantCmd { 78 | t.Errorf("resolveWithDockerInspect() cmd = %v, wantCmd %v", gotCmd, tt.wantCmd) 79 | return 80 | } 81 | if got != tt.want { 82 | t.Errorf("resolveWithDockerInspect() = %v, want %v", got, tt.want) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func Test_resolveWithDockerManifestInspect(t *testing.T) { 89 | type args struct { 90 | imageRef string 91 | } 92 | tests := []struct { 93 | name string 94 | args args 95 | cmdOut []byte 96 | cmdErr error 97 | want string 98 | wantCmd string 99 | wantErr bool 100 | }{ 101 | { 102 | name: "image not found", 103 | args: args{"registry:5000/path/hello:latest"}, 104 | cmdOut: []byte(""), 105 | cmdErr: &exec.ExitError{ 106 | Stderr: []byte("no such manifest: registry:5000/path/hello:latest\n"), 107 | }, 108 | want: "", 109 | wantCmd: "docker manifest inspect --verbose registry:5000/path/hello:latest", 110 | wantErr: false, 111 | }, 112 | { 113 | name: "experimental cli features not enabled", 114 | args: args{"registry:5000/path/hello:latest"}, 115 | cmdOut: []byte(""), 116 | cmdErr: &exec.ExitError{ 117 | Stderr: []byte("docker manifest inspect is only supported on a Docker cli with experimental cli features enabled\n"), 118 | }, 119 | want: "", 120 | wantCmd: "docker manifest inspect --verbose registry:5000/path/hello:latest", 121 | wantErr: true, 122 | }, 123 | { 124 | name: "docker command not found", 125 | args: args{"registry:5000/path/hello:latest"}, 126 | cmdOut: []byte(""), 127 | cmdErr: errors.New("docker inspect: exec: \"docker\": executable file not found in $PATH"), 128 | want: "", 129 | wantCmd: "docker manifest inspect --verbose registry:5000/path/hello:latest", 130 | wantErr: true, 131 | }, 132 | { 133 | name: "manifest list does not contain linux amd64", 134 | args: args{"openjdk:latest"}, 135 | cmdOut: []byte(`[ 136 | { 137 | "Ref": "docker.io/library/openjdk:latest@sha256:ff3da04131714a6e03d02684a33a3858e622923344534de87ff453d03181337a", 138 | "Descriptor": { 139 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 140 | "digest": "sha256:ff3da04131714a6e03d02684a33a3858e622923344534de87ff453d03181337a", 141 | "size": 2000, 142 | "platform": { 143 | "architecture": "arm", 144 | "os": "linux", 145 | "variant": "v5" 146 | } 147 | } 148 | } 149 | ]`), 150 | cmdErr: nil, 151 | want: "", 152 | wantCmd: "docker manifest inspect --verbose openjdk:latest", 153 | wantErr: false, 154 | }, 155 | { 156 | name: "success with manifest list", 157 | args: args{"openjdk:latest"}, 158 | cmdOut: []byte(`[ 159 | { 160 | "Ref": "docker.io/library/openjdk:latest@sha256:c7381bfd53670f1211314885b03b98f5e13fddf6958afeec61092b07c56ddef1", 161 | "Descriptor": { 162 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 163 | "digest": "sha256:c7381bfd53670f1211314885b03b98f5e13fddf6958afeec61092b07c56ddef1", 164 | "size": 2000, 165 | "platform": { 166 | "architecture": "amd64", 167 | "os": "linux" 168 | } 169 | } 170 | }, 171 | { 172 | "Ref": "docker.io/library/openjdk:latest@sha256:ff3da04131714a6e03d02684a33a3858e622923344534de87ff453d03181337a", 173 | "Descriptor": { 174 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 175 | "digest": "sha256:ff3da04131714a6e03d02684a33a3858e622923344534de87ff453d03181337a", 176 | "size": 2000, 177 | "platform": { 178 | "architecture": "arm", 179 | "os": "linux", 180 | "variant": "v5" 181 | } 182 | } 183 | } 184 | ]`), 185 | cmdErr: nil, 186 | want: "openjdk@sha256:c7381bfd53670f1211314885b03b98f5e13fddf6958afeec61092b07c56ddef1", 187 | wantCmd: "docker manifest inspect --verbose openjdk:latest", 188 | wantErr: false, 189 | }, 190 | { 191 | name: "success", 192 | args: args{"registry:5000/path/hello:latest"}, 193 | cmdOut: []byte(`{ 194 | "Ref": "registry:5000/path/hello:latest", 195 | "Descriptor": { 196 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 197 | "digest": "sha256:2d8b22d01ca51eef988ff3ae8dcf37c182553b662ea47d3d62ce8208a3b83aef", 198 | "size": 3661, 199 | "platform": { 200 | "architecture": "amd64", 201 | "os": "linux" 202 | } 203 | } 204 | }`), 205 | cmdErr: nil, 206 | want: "registry:5000/path/hello@sha256:2d8b22d01ca51eef988ff3ae8dcf37c182553b662ea47d3d62ce8208a3b83aef", 207 | wantCmd: "docker manifest inspect --verbose registry:5000/path/hello:latest", 208 | wantErr: false, 209 | }, 210 | } 211 | for _, tt := range tests { 212 | t.Run(tt.name, func(t *testing.T) { 213 | var gotCmd string 214 | execCmdMock := func(name string, arg ...string) ([]byte, error) { 215 | gotCmd = name + " " + strings.Join(arg, " ") 216 | return tt.cmdOut, tt.cmdErr 217 | } 218 | 219 | got, err := resolveWithDockerManifestInspect(tt.args.imageRef, execCmdMock) 220 | if (err != nil) != tt.wantErr { 221 | t.Errorf("resolveWithDockerManifestInspect() error = %v, wantErr %v", err, tt.wantErr) 222 | return 223 | } 224 | if gotCmd != tt.wantCmd { 225 | t.Errorf("resolveWithDockerInspect() cmd = %v, wantCmd %v", gotCmd, tt.wantCmd) 226 | return 227 | } 228 | if got != tt.want { 229 | t.Errorf("resolveWithDockerManifestInspect() = %v, want %v", got, tt.want) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | func Test_removeTagAndDigest(t *testing.T) { 236 | type args struct { 237 | imageRef string 238 | } 239 | tests := []struct { 240 | name string 241 | args args 242 | want string 243 | }{ 244 | { 245 | name: "only name", 246 | args: args{"hello"}, 247 | want: "hello", 248 | }, 249 | { 250 | name: "only name with domain and path", 251 | args: args{"registry:5000/path/hello"}, 252 | want: "registry:5000/path/hello", 253 | }, 254 | { 255 | name: "tag", 256 | args: args{"hello:latest"}, 257 | want: "hello", 258 | }, 259 | { 260 | name: "tag with domain and path", 261 | args: args{"registry:5000/path/hello:latest"}, 262 | want: "registry:5000/path/hello", 263 | }, 264 | { 265 | name: "digest", 266 | args: args{"hello@sha256:e3227b2d3d50d02fb80194e6b82ca2df1ece0608e2c1fa9e73356775c6a7c095"}, 267 | want: "hello", 268 | }, 269 | { 270 | name: "digest with domain and path", 271 | args: args{"registry:5000/path/hello@sha256:e3227b2d3d50d02fb80194e6b82ca2df1ece0608e2c1fa9e73356775c6a7c095"}, 272 | want: "registry:5000/path/hello", 273 | }, 274 | { 275 | name: "tag and digest", 276 | args: args{"hello:latest@sha256:e3227b2d3d50d02fb80194e6b82ca2df1ece0608e2c1fa9e73356775c6a7c095"}, 277 | want: "hello", 278 | }, 279 | { 280 | name: "tag and digest with domain and path", 281 | args: args{"registry:5000/path/hello:latest@sha256:e3227b2d3d50d02fb80194e6b82ca2df1ece0608e2c1fa9e73356775c6a7c095"}, 282 | want: "registry:5000/path/hello", 283 | }, 284 | } 285 | for _, tt := range tests { 286 | t.Run(tt.name, func(t *testing.T) { 287 | if got := removeTagAndDigest(tt.args.imageRef); got != tt.want { 288 | t.Errorf("removeTagAndDigest() = %v, want %v", got, tt.want) 289 | } 290 | }) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | if [ "$GITHUB_ACTIONS" = "true" ]; then 6 | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then 7 | version="${GITHUB_REF:11}" 8 | echo "KYML_RELEASE_VERSION=$version" >>$GITHUB_ENV 9 | else 10 | version="untagged" 11 | fi 12 | else 13 | version=${1:?"Version (arg 1) missing"} 14 | fi 15 | 16 | echo "Version: $version" 17 | 18 | ldflags="-X github.com/frigus02/kyml/pkg/commands.version=$version" 19 | 20 | echo "Build darwin" 21 | GOARCH=amd64 GOOS=darwin go build -o "bin/kyml_${version}_darwin_amd64" -ldflags "$ldflags" 22 | GOARCH=arm64 GOOS=darwin go build -o "bin/kyml_${version}_darwin_arm64" -ldflags "$ldflags" 23 | 24 | echo "Build linux" 25 | GOARCH=amd64 GOOS=linux go build -o "bin/kyml_${version}_linux_amd64" -ldflags "$ldflags" 26 | 27 | echo "Build windows" 28 | GOARCH=amd64 GOOS=windows go build -o "bin/kyml_${version}_windows_amd64.exe" -ldflags "$ldflags" 29 | 30 | echo "Create checksums" 31 | ( 32 | cd bin/ 33 | sha256sum "kyml_${version}"* >checksums.txt 34 | ) 35 | -------------------------------------------------------------------------------- /scripts/release.md: -------------------------------------------------------------------------------- 1 | # Release instructions 2 | 3 | To release a new version of kyml, follow these steps: 4 | 5 | 1. Choose a version number in the following format: 6 | 7 | ```sh 8 | VERSION=$(date -u +%Y%m%d) 9 | ``` 10 | 11 | If you need to release a second version on the same day, add a dot followed by an incrementing number to the end, e.g.: 20181228.2, 20181228.3, ... 12 | 13 | 1. Create a branch. 14 | 15 | ```sh 16 | git checkout -b version-$VERSION 17 | ``` 18 | 19 | 1. Update the [CHANGELOG.md](../CHANGELOG.md). 20 | 21 | 1. Update the version in the installation instructions in the [README.md](../README.md). 22 | 23 | 1. Commit and push the branch. Create a pull request for the branch. Then wait for the builds to succeed. 24 | 25 | ```sh 26 | git commit -m "Update changelog and readme for v$VERSION" 27 | ``` 28 | 29 | 1. Tag the commit with `v$VERSION`. 30 | 31 | ```sh 32 | git tag v$VERSION 33 | ``` 34 | 35 | 1. Push the tag. This will trigger another build, which creates a release on GitHub and uploads artifacts. 36 | 37 | ```sh 38 | git push --tags 39 | ``` 40 | 41 | 1. Merge the pull request. 42 | 43 | 1. Update version and checksums in Homebrew formula [frigus02/tap/kyml](https://github.com/frigus02/homebrew-tap/blob/main/kyml.rb). 44 | --------------------------------------------------------------------------------