├── .github ├── dependabot.yml └── workflows │ ├── docker-build.yaml │ └── go.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── image-updater │ └── main.go ├── deploy └── deployment.yaml ├── example └── config.yaml ├── go.mod ├── go.sum ├── pkg ├── applier │ ├── apply.go │ └── apply_test.go ├── cmd │ ├── client.go │ ├── http.go │ ├── pubsub.go │ ├── root.go │ └── update.go ├── config │ ├── config.go │ ├── config_test.go │ └── testdata │ │ └── config.yaml ├── handler │ ├── handler.go │ ├── handler_test.go │ └── testdata │ │ └── push_hook.json ├── hooks │ ├── docker │ │ ├── hook.go │ │ ├── hook_test.go │ │ └── testdata │ │ │ └── push_event.json │ ├── gcr │ │ ├── hook.go │ │ ├── hook_test.go │ │ └── testdata │ │ │ └── push_event.json │ ├── interface.go │ └── quay │ │ ├── hook.go │ │ ├── hook_test.go │ │ └── testdata │ │ └── push_hook.json ├── names │ ├── generator.go │ ├── generator_test.go │ └── interface.go └── pubsubhandler │ ├── handler.go │ ├── handler_test.go │ ├── interface.go │ └── testdata │ └── push_event.json ├── tekton ├── configuring-custom-ca.md └── image-updater.yaml └── test └── errors.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | if: github.repository == 'gitops-tools/image-updater' 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v2 17 | 18 | - name: build docker image and push 19 | uses: docker/build-push-action@v1 20 | with: 21 | username: ${{ secrets.DOCKER_USERNAME }} 22 | password: ${{ secrets.DOCKER_PASSWORD }} 23 | repository: bigkevmcd/image-updater 24 | tag_with_ref: true 25 | tag_with_sha: true 26 | tags: latest 27 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.18 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test -v .//... 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at bigkevmcd@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS build 2 | WORKDIR /go/src 3 | COPY . /go/src 4 | RUN go build ./cmd/image-updater 5 | 6 | FROM registry.access.redhat.com/ubi8/ubi-minimal 7 | WORKDIR /root/ 8 | COPY --from=build /go/src/image-updater . 9 | EXPOSE 8080 10 | ENTRYPOINT ["/root/image-updater"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # image-updater ![Go](https://github.com/gitops-tools/image-updater/workflows/Go/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/gitops-tools/image-updater)](https://goreportcard.com/report/github.com/gitops-tools/image-updater) 2 | 3 | This is a small tool and service for updating YAML files with image references, 4 | to simplify continuous deployment pipelines. 5 | 6 | It updates a YAML file in a Git repository, and optionally opens a Pull Request. 7 | 8 | ## Command-line tool 9 | 10 | ```shell 11 | $ ./image-updater --help 12 | Update YAML files in a Git service, with optional automated Pull Requests 13 | 14 | Usage: 15 | image-updater [command] 16 | 17 | Available Commands: 18 | help Help about any command 19 | http update repositories in response to image hooks 20 | pubsub update repositories in response to gcr pubsub events 21 | update update a repository configuration 22 | 23 | Flags: 24 | -h, --help help for image-updater 25 | 26 | Use "image-updater [command] --help" for more information about a command. 27 | ``` 28 | 29 | There are three sub-commands, `http`, `pubsub` and `update`. 30 | 31 | `http` provides a Webhook service, `pubsub` subscribes to pubsub events and `update` will perform the same 32 | functionality from the command-line. 33 | 34 | ## Update tool 35 | 36 | This requires a `AUTH_TOKEN` environment variable with a token. 37 | 38 | ```shell 39 | $ ./image-updater update --file-path service-a/deployment.yaml --image-repo quay.io/myorg/my-image --source-repo mysource/my-repo --new-image-url quay.io/myorg/my-image:v1.1.0 --update-key spec.template.spec.containers.0.image 40 | ``` 41 | 42 | This would update a file `service-a/deployment.yaml` in a GitHub repo at `mysource/my-repo`, changing the `spec.template.spec.containers.0.image` key in the file to `quay.io/myorg/my-image:v1.1.0`, the PR will indicate that this is an update from `quay.io/myorg/my-image`. 43 | 44 | If you need to access a private GitLab or GitHub installation, you can provide 45 | the `--api-endpoint` e.g. 46 | 47 | ```shell 48 | $ ./image-updater update --file-path service-a/deployment.yaml --image-repo quay.io/myorg/my-image --source-repo mysource/my-repo --new-image-url quay.io/myorg/my-image:v1.1.0 --update-key spec.template.spec.containers.0.image 49 | ``` 50 | 51 | For the HTTP service, you will likely need to adapt the deployment. 52 | 53 | You can also opt to allow for insecure TLS access with `--insecure`. 54 | 55 | ## Webhook Service 56 | 57 | This is a micro-service for updating Git Repos when a hook is received indicating that a new image has been pushed from an image repository. 58 | 59 | This currently supports receiving hooks from Docker and Quay.io. 60 | 61 | ### WARNING 62 | 63 | Neither Docker Hub nor Quay.io provide a way for receivers to authenticate Webhooks, which makes this insecure, a malicious user could trigger the creation of pull requests in your git hosting service. 64 | 65 | Please understand the risks of using this component. 66 | 67 | ## Pubsub Service 68 | Similarly to the Webhook service, the pubsub services allows to update Git Repos when a pubsub Event is received. 69 | 70 | This currently supports Events from [Google Cloud Registry](https://cloud.google.com/container-registry/docs/configuring-notifications). 71 | 72 | It requires two arguments `--project-id` and `--subscription-name`. See [below](#google-container-registry-setup) for more details on how to setup the subscription. 73 | 74 | ## Configuration 75 | 76 | Both the Webhook and Pubsub service uses a really simple configuration: 77 | 78 | ```yaml 79 | repositories: 80 | - name: testing/repo-image 81 | sourceRepo: my-org/my-project 82 | sourceBranch: main 83 | filePath: service-a/deployment.yaml 84 | updateKey: spec.template.spec.containers.0.image 85 | branchGenerateName: repo-imager- 86 | tagMatch: "^main-.*" 87 | ``` 88 | 89 | This is a single repository configuration, Repo Push notifications from the 90 | image `testing/repo-image`, will trigger an update in the repo 91 | `my-org/my-project`. 92 | 93 | The change will be based off the `main` branch, and updating the file 94 | `service-a/deployment.yaml`. 95 | 96 | Within that file, the `spec.template.spec.containers.0.image` field will be replaced 97 | with the incoming image. 98 | 99 | A new branch will be created based on the `branchGenerateName` field, which 100 | would look something like `repo-imager-kXzdf`. 101 | 102 | The presence of the `tagMatch` field means that it should only apply the update, 103 | if the tag being changed matches this regular expression, in this case, tags 104 | like "main-c1f79ab" would match, but "test-pr-branch-c1f79ab" would not. 105 | 106 | ### Updating the sourceBranch directly 107 | 108 | If no value is provided for `branchGenerateName`, then the `sourceBranch` will 109 | be updated directly, this means that if you use `main`, then the token must 110 | have access to push a change directly to `main`. 111 | 112 | ### Creating the configuration 113 | 114 | The tool reads a YAML definition, which in the provided `Deployment` is mounted 115 | in from a `ConfigMap`. 116 | 117 | ```shell 118 | $ kubectl create configmap image-updater-config --from-file=config.yaml 119 | ``` 120 | 121 | The default deployment requires a secret to expose the `GITHUB_TOKEN` to the 122 | service. 123 | 124 | 125 | ```shell 126 | $ export GITHUB_TOKEN= 127 | $ kubectl create secret generic image-updater-secret --from-literal=token=$GITHUB_TOKEN 128 | ``` 129 | 130 | ## Deployment 131 | 132 | A Kubernetes `Deployment` is provided in [./deploy/deployment.yaml](./deploy/deployment.yaml). 133 | 134 | The service is **not** dependent on being executed within a Kubernetes cluster. 135 | 136 | ## Choosing a hook parser 137 | 138 | By default, this accepts hooks from Docker hub but the deployment can easily be 139 | changed to support Quay.io. 140 | 141 | The `--parser` command-line option chooses which of the supported (Quay, Docker) 142 | hook formats to parse. 143 | 144 | 145 | ## Exposing the Handler 146 | 147 | The Service exposes a Hook handler at `/` on port 8080 that handles the 148 | configured hook type. 149 | 150 | ## Tekton 151 | 152 | A Tekton task is provided in [./tekton](./tekton) which allows you to apply 153 | updates to repos from a Tekton pipeline run. 154 | 155 | 156 | ## Google Container registry setup 157 | ```bash 158 | gcloud pubsub topics create gcr 159 | gcloud pubsub subscriptions create gcr-image-updater --topic projects/$GOOGLE_PROJECT/topics/gcr 160 | 161 | gcloud iam service-accounts create 162 | gcloud iam service-accounts keys create credentials.json \ 163 | --iam-account $SA_NAME@$GOOGLE_PROJECT.iam.gserviceaccount.com 164 | 165 | gcloud pubsub subscriptions add-iam-policy-binding gcr-image-updater \ 166 | --member=serviceAccount:$SA_NAME@$GOOGLE_PROJECT.iam.gserviceaccount.com --role=roles/pubsub.subscriber 167 | ``` 168 | 169 | You then need to set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path the generated `credentials.json` file. 170 | 171 | ## Building 172 | 173 | A `Dockerfile` is provided for building a container, but otherwise: 174 | 175 | ```shell 176 | $ go build ./cmd/image-updater 177 | ``` 178 | 179 | ## Docker images 180 | 181 | Images are available at `bigkevmcd/image-updater:latest` or based on the tag e.g `bigkevmcd/image-updater:v0.0.2` 182 | 183 | ## Testing 184 | 185 | ```shell 186 | $ go test ./... 187 | ``` 188 | -------------------------------------------------------------------------------- /cmd/image-updater/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gitops-tools/image-updater/pkg/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /deploy/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: image-updater-http 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: image-updater-http 11 | template: 12 | metadata: 13 | labels: 14 | app.kubernetes.io/name: image-updater-http 15 | spec: 16 | containers: 17 | - name: image-updater-http 18 | image: bigkevmcd/image-updater:latest 19 | imagePullPolicy: Always 20 | args: ["http", "--parser", "docker"] 21 | volumeMounts: 22 | - name: config-volume 23 | mountPath: /etc/image-updater 24 | env: 25 | - name: AUTH_TOKEN 26 | valueFrom: 27 | secretKeyRef: 28 | name: image-updater-secret 29 | key: token 30 | volumes: 31 | - name: config-volume 32 | configMap: 33 | name: image-updater-config 34 | --- 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: image-updater-http 39 | namespace: default 40 | spec: 41 | type: ClusterIP 42 | selector: 43 | app.kubernetes.io/name: image-updater-http 44 | ports: 45 | - protocol: TCP 46 | port: 8080 47 | -------------------------------------------------------------------------------- /example/config.yaml: -------------------------------------------------------------------------------- 1 | repositories: 2 | - name: quay-org/my-image 3 | sourceRepo: my-github-org/my-repo 4 | sourceBranch: master 5 | filePath: deploy/person.yaml 6 | updateKey: spec.template.spec.containers.0.image 7 | branchGenerateName: repo-imager- 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gitops-tools/image-updater 2 | 3 | go 1.20 4 | 5 | require ( 6 | cloud.google.com/go/pubsub v1.33.0 7 | github.com/gitops-tools/pkg v0.1.0 8 | github.com/go-logr/logr v1.3.0 9 | github.com/go-logr/zapr v1.3.0 10 | github.com/google/go-cmp v0.6.0 11 | github.com/jenkins-x/go-scm v1.14.14 12 | github.com/spf13/cobra v1.7.0 13 | github.com/spf13/viper v1.17.0 14 | go.uber.org/zap v1.26.0 15 | golang.org/x/oauth2 v0.13.0 16 | sigs.k8s.io/yaml v1.4.0 17 | ) 18 | 19 | require ( 20 | cloud.google.com/go v0.110.7 // indirect 21 | cloud.google.com/go/compute v1.23.0 // indirect 22 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 23 | cloud.google.com/go/iam v1.1.1 // indirect 24 | code.gitea.io/sdk/gitea v0.14.0 // indirect 25 | github.com/bluekeyes/go-gitdiff v0.7.1 // indirect 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 27 | github.com/fsnotify/fsnotify v1.6.0 // indirect 28 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 29 | github.com/golang/protobuf v1.5.3 // indirect 30 | github.com/google/s2a-go v0.1.7 // indirect 31 | github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect 32 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 33 | github.com/hashicorp/go-version v1.3.0 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 36 | github.com/magiconair/properties v1.8.7 // indirect 37 | github.com/mitchellh/copystructure v1.2.0 // indirect 38 | github.com/mitchellh/mapstructure v1.5.0 // indirect 39 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 40 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 43 | github.com/sagikazarmark/locafero v0.3.0 // indirect 44 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 45 | github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260 // indirect 46 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect 47 | github.com/sirupsen/logrus v1.9.3 // indirect 48 | github.com/sourcegraph/conc v0.3.0 // indirect 49 | github.com/spf13/afero v1.10.0 // indirect 50 | github.com/spf13/cast v1.5.1 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/stretchr/testify v1.8.4 // indirect 53 | github.com/subosito/gotenv v1.6.0 // indirect 54 | github.com/tidwall/gjson v1.14.2 // indirect 55 | github.com/tidwall/match v1.1.1 // indirect 56 | github.com/tidwall/pretty v1.2.0 // indirect 57 | github.com/tidwall/sjson v1.2.5 // indirect 58 | go.opencensus.io v0.24.0 // indirect 59 | go.uber.org/multierr v1.10.0 // indirect 60 | golang.org/x/crypto v0.17.0 // indirect 61 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 62 | golang.org/x/net v0.17.0 // indirect 63 | golang.org/x/sync v0.3.0 // indirect 64 | golang.org/x/sys v0.15.0 // indirect 65 | golang.org/x/text v0.14.0 // indirect 66 | google.golang.org/api v0.143.0 // indirect 67 | google.golang.org/appengine v1.6.7 // indirect 68 | google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect 69 | google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect 70 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect 71 | google.golang.org/grpc v1.58.3 // indirect 72 | google.golang.org/protobuf v1.31.0 // indirect 73 | gopkg.in/ini.v1 v1.67.0 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | k8s.io/apimachinery v0.27.3 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /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.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= 21 | cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= 22 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 23 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 24 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 25 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 26 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 27 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 28 | cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= 29 | cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= 30 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= 31 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 32 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 33 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 34 | cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= 35 | cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= 36 | cloud.google.com/go/kms v1.15.0 h1:xYl5WEaSekKYN5gGRyhjvZKM22GVBBCzegGNVPy+aIs= 37 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 38 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 39 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 40 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 41 | cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= 42 | cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= 43 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 44 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 45 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 46 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 47 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 48 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 49 | code.gitea.io/sdk/gitea v0.14.0 h1:m4J352I3p9+bmJUfS+g0odeQzBY/5OXP91Gv6D4fnJ0= 50 | code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= 51 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 52 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 53 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 54 | github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ= 55 | github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= 56 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 57 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 58 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 59 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 60 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 61 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 62 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 63 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 64 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 65 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 66 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 67 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 68 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 69 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 70 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 71 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 72 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 73 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 74 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 75 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 76 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 77 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 78 | github.com/gitops-tools/pkg v0.1.0 h1:atKTGUjGEEvkSX+HGCzI76rHRB84+nr77ll8kyJY3Nk= 79 | github.com/gitops-tools/pkg v0.1.0/go.mod h1:c+ZMQS6qVn3+HfJ3Hl04ARo7zxD30ackJnV60UlLC5s= 80 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 81 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 82 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 83 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 84 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 85 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 86 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 87 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 88 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 89 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 90 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 91 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 92 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 93 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 94 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 95 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 96 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 97 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 98 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 99 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 100 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 101 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 102 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 103 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 104 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 105 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 106 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 107 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 108 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 109 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 110 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 111 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 112 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 113 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 114 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 115 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 116 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 117 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 118 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 119 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 121 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 122 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 123 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 124 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 125 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 132 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 133 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 134 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 135 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 136 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 137 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 138 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 139 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 140 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 141 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 142 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 145 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 146 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 147 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 148 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 149 | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= 150 | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 151 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 152 | github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ= 153 | github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 154 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 155 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 156 | github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= 157 | github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= 158 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 159 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 160 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 161 | github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= 162 | github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 163 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 164 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 165 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 166 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 167 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 168 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 169 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 170 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 171 | github.com/jenkins-x/go-scm v1.14.14 h1:a4c3z4+FVPMWMl59hgdLZNbnbc0Z0/Ln6fHXS0hLAyY= 172 | github.com/jenkins-x/go-scm v1.14.14/go.mod h1:MR/WVGUSEqED4SP/lWaRKtks/vYGtylFueDr1FLogYg= 173 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 174 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 175 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 176 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 177 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 178 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 179 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 180 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 181 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 182 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 183 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 184 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 185 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 186 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 187 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 188 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 189 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 190 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 191 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 192 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 193 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 194 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 195 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 196 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 197 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 198 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 199 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 200 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 201 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 202 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 203 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 204 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 205 | github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= 206 | github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= 207 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 208 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 209 | github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260 h1:xKXiRdBUtMVp64NaxACcyX4kvfmHJ9KrLU+JvyB1mdM= 210 | github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= 211 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= 212 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= 213 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 214 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 215 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 216 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 217 | github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= 218 | github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 219 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 220 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 221 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 222 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 223 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 224 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 225 | github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= 226 | github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= 227 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 228 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 229 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 230 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 231 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 232 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 233 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 234 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 235 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 236 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 237 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 238 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 239 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 240 | github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= 241 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 242 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 243 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 244 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 245 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 246 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 247 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 248 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 249 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 250 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 251 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 252 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 253 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 254 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 255 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 256 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 257 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 258 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 259 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 260 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 261 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 262 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 263 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 264 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 265 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 266 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 267 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 268 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 269 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 270 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 271 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 272 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 273 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 274 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 275 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 276 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 277 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 278 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 279 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 280 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 281 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 282 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 283 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 284 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 285 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 286 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 287 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 288 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 289 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 290 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 291 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 292 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 293 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 294 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 295 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 296 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 297 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 298 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 299 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 300 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 301 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 302 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 303 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 304 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 305 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 306 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 307 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 308 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 309 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 310 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 311 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 312 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 313 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 314 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 315 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 316 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 317 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 318 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 319 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 320 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 321 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 322 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 323 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 324 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 325 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 326 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 327 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 328 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 329 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 330 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 331 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 332 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 333 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 334 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 335 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 336 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 337 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 338 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 339 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 340 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 341 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 342 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 343 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 344 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 345 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 346 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 347 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 348 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 349 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 350 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 351 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 352 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 353 | golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= 354 | golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= 355 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 356 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 357 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 358 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 359 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 360 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 361 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 362 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 363 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 364 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 365 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 366 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 367 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 368 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 369 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 402 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 403 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 404 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 405 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 406 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 407 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 408 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 409 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 410 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 411 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 412 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 413 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 414 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 415 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 416 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 417 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 418 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 419 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 420 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 421 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 422 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 423 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 424 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 425 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 426 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 427 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 428 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 429 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 430 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 431 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 432 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 433 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 434 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 435 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 436 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 437 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 438 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 439 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 440 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 441 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 442 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 443 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 444 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 445 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 446 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 447 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 448 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 449 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 450 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 451 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 452 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 453 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 454 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 455 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 456 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 457 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 458 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 459 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 460 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 461 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 462 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 463 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 464 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 465 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 466 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 467 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 468 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 469 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 470 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 471 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 472 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 473 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 474 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 475 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 476 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 477 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 478 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 479 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 480 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 481 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 482 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 483 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 484 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 485 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 486 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 487 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 488 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 489 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 490 | google.golang.org/api v0.143.0 h1:o8cekTkqhywkbZT6p1UHJPZ9+9uuCAJs/KYomxZB8fA= 491 | google.golang.org/api v0.143.0/go.mod h1:FoX9DO9hT7DLNn97OuoZAGSDuNAXdJRuGK98rSUgurk= 492 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 493 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 494 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 495 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 496 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 497 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 498 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 499 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 500 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 501 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 502 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 503 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 504 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 505 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 506 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 507 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 508 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 509 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 510 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 511 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 512 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 513 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 514 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 515 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 516 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 517 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 518 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 519 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 520 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 521 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 522 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 523 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 524 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 525 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 526 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 527 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 528 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 529 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 530 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 531 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 532 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 533 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 534 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 535 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 536 | google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= 537 | google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= 538 | google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= 539 | google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= 540 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= 541 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= 542 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 543 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 544 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 545 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 546 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 547 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 548 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 549 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 550 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 551 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 552 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 553 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 554 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 555 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 556 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 557 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 558 | google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= 559 | google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= 560 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 561 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 562 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 563 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 564 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 565 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 566 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 567 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 568 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 569 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 570 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 571 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 572 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 573 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 574 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 575 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 576 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 577 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 578 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= 579 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 580 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 581 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 582 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 583 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 584 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 585 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 586 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 587 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 588 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 589 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 590 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 591 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 592 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 593 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 594 | k8s.io/api v0.24.3 h1:tt55QEmKd6L2k5DP6G/ZzdMQKvG5ro4H4teClqm0sTY= 595 | k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM= 596 | k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= 597 | k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= 598 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= 599 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 600 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 601 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 602 | sigs.k8s.io/controller-runtime v0.12.3 h1:FCM8xeY/FI8hoAfh/V4XbbYMY20gElh9yh+A98usMio= 603 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 604 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 605 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 606 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 607 | -------------------------------------------------------------------------------- /pkg/applier/apply.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "regexp" 8 | "time" 9 | 10 | "github.com/gitops-tools/image-updater/pkg/config" 11 | "github.com/gitops-tools/image-updater/pkg/hooks" 12 | "github.com/gitops-tools/pkg/client" 13 | "github.com/gitops-tools/pkg/updater" 14 | "github.com/go-logr/logr" 15 | ) 16 | 17 | var timeSeed = rand.New(rand.NewSource(time.Now().UnixNano())) 18 | 19 | // New creates and returns a new Applier. 20 | func New(l logr.Logger, c client.GitClient, cfgs *config.RepoConfiguration, opts ...updater.UpdaterFunc) *Applier { 21 | return &Applier{configs: cfgs, log: l, updater: updater.New(l, c, opts...)} 22 | } 23 | 24 | // Applier can update a Git repo with an updated version of a file based on a 25 | // RepositoryPushHook. 26 | type Applier struct { 27 | configs *config.RepoConfiguration 28 | log logr.Logger 29 | updater *updater.Updater 30 | } 31 | 32 | // UpdateFromHook takes the incoming hook and triggers an update based on the 33 | // configuration for the repo in the hook (if one matches). 34 | func (u *Applier) UpdateFromHook(ctx context.Context, h hooks.PushEvent) error { 35 | cfg := u.configs.Find(h.EventRepository()) 36 | if cfg == nil { 37 | u.log.Info("failed to find repo", "name", h.EventRepository()) 38 | return nil 39 | } 40 | if cfg.TagMatch != "" { 41 | re, err := regexp.Compile(cfg.TagMatch) 42 | if err != nil { 43 | return fmt.Errorf("failed to compile TagMatch regular expression: %s", err) 44 | } 45 | if !re.MatchString(h.EventTag()) { 46 | u.log.Info("failed to match tag", "tag", h.EventTag(), "tagMatch", cfg.TagMatch) 47 | return nil 48 | } 49 | } 50 | u.log.Info("found repo", "name", h.EventRepository(), "newURL", h.PushedImageURL()) 51 | return u.UpdateRepository(ctx, cfg, h.PushedImageURL()) 52 | } 53 | 54 | // UpdateRepository does the job of fetching the existing file, updating it, and 55 | // then optionally creating a PR. 56 | func (u *Applier) UpdateRepository(ctx context.Context, cfg *config.Repository, newURL string) error { 57 | ci := updater.CommitInput{ 58 | Repo: cfg.SourceRepo, 59 | Filename: cfg.FilePath, 60 | Branch: cfg.SourceBranch, 61 | BranchGenerateName: cfg.BranchGenerateName, 62 | CommitMessage: "Automatic update because an image was updated", 63 | } 64 | 65 | newBranch, err := u.updater.ApplyUpdateToFile(ctx, ci, updater.UpdateYAML(cfg.UpdateKey, newURL)) 66 | if err != nil { 67 | u.log.Error(err, "failed to get file from repo") 68 | return err 69 | } 70 | u.log.Info("updated branch with image", "image", newURL, "branch", newBranch) 71 | 72 | // If we modified the original branch... 73 | if newBranch == cfg.SourceBranch { 74 | return nil 75 | } 76 | 77 | pullRequestInput := updater.PullRequestInput{ 78 | Title: "Automated image update", 79 | Body: fmt.Sprintf("Automated update from %q", cfg.Name), 80 | Repo: cfg.SourceRepo, 81 | NewBranch: newBranch, 82 | SourceBranch: cfg.SourceBranch, 83 | } 84 | 85 | pr, err := u.updater.CreatePR(ctx, pullRequestInput) 86 | if err != nil { 87 | return fmt.Errorf("failed to create pull request in repo %s: %w", cfg.SourceRepo, err) 88 | } 89 | u.log.Info("created PullRequest", "link", pr.Link) 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/applier/apply_test.go: -------------------------------------------------------------------------------- 1 | package applier 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/gitops-tools/image-updater/pkg/config" 10 | "github.com/gitops-tools/image-updater/pkg/hooks/quay" 11 | "github.com/gitops-tools/pkg/client/mock" 12 | "github.com/gitops-tools/pkg/updater" 13 | "github.com/go-logr/zapr" 14 | "github.com/jenkins-x/go-scm/scm" 15 | "go.uber.org/zap" 16 | "go.uber.org/zap/zaptest" 17 | ) 18 | 19 | const ( 20 | testQuayRepo = "mynamespace/repository" 21 | testGitHubRepo = "testorg/testrepo" 22 | testFilePath = "environments/test/services/service-a/test.yaml" 23 | ) 24 | 25 | func TestUpdaterWithUnknownRepo(t *testing.T) { 26 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 27 | m := mock.New(t) 28 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 29 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 30 | applier := makeApplier(t, m, createConfigs()) 31 | hook := createHook() 32 | hook.Repository = "unknown/repo" 33 | 34 | err := applier.UpdateFromHook(context.Background(), hook) 35 | 36 | // A non-matching repo is not considered an error. 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 41 | if s := string(updated); s != "" { 42 | t.Fatalf("update failed, got %#v, want %#v", s, "") 43 | } 44 | m.RefuteBranchCreated(testGitHubRepo, "test-branch-a", testSHA) 45 | 46 | m.RefutePullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 47 | Title: fmt.Sprintf("Image %s updated", testQuayRepo), 48 | Body: "Automated Image Update", 49 | Head: "test-branch-a", 50 | Base: "master", 51 | }) 52 | } 53 | 54 | func TestUpdaterWithNonMatchingTag(t *testing.T) { 55 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 56 | m := mock.New(t) 57 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 58 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 59 | configs := createConfigs() 60 | configs.Repositories[0].BranchGenerateName = "" 61 | configs.Repositories[0].TagMatch = "^v.*" 62 | applier := makeApplier(t, m, configs) 63 | hook := createHook() 64 | 65 | err := applier.UpdateFromHook(context.Background(), hook) 66 | 67 | // A non-matching tag is not considered an error. 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | m.AssertNoInteractions() 73 | } 74 | 75 | func TestUpdaterWithKnownRepo(t *testing.T) { 76 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 77 | m := mock.New(t) 78 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 79 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 80 | applier := makeApplier(t, m, createConfigs()) 81 | hook := createHook() 82 | 83 | err := applier.UpdateFromHook(context.Background(), hook) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 89 | want := "test:\n image: quay.io/testorg/repo:production\n" 90 | if s := string(updated); s != want { 91 | t.Fatalf("update failed, got %#v, want %#v", s, want) 92 | } 93 | m.AssertBranchCreated(testGitHubRepo, "test-branch-a", testSHA) 94 | m.AssertPullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 95 | Title: "Automated image update", 96 | Body: fmt.Sprintf("Automated update from %q", testQuayRepo), 97 | Head: "test-branch-a", 98 | Base: "master", 99 | }) 100 | } 101 | 102 | // With no name-generator, the change should be made to master directly, rather 103 | // than going through a PullRequest. 104 | func TestUpdaterWithNoNameGenerator(t *testing.T) { 105 | sourceBranch := "production" 106 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 107 | m := mock.New(t) 108 | m.AddFileContents(testGitHubRepo, testFilePath, sourceBranch, []byte("test:\n image: old-image\n")) 109 | m.AddBranchHead(testGitHubRepo, sourceBranch, testSHA) 110 | configs := createConfigs() 111 | configs.Repositories[0].BranchGenerateName = "" 112 | configs.Repositories[0].SourceBranch = sourceBranch 113 | applier := makeApplier(t, m, configs) 114 | hook := createHook() 115 | 116 | err := applier.UpdateFromHook(context.Background(), hook) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, sourceBranch) 122 | want := "test:\n image: quay.io/testorg/repo:production\n" 123 | if s := string(updated); s != want { 124 | t.Fatalf("update failed, got %#v, want %#v", s, want) 125 | } 126 | m.AssertNoBranchesCreated() 127 | m.AssertNoPullRequestsCreated() 128 | } 129 | 130 | func TestUpdaterWithMissingFile(t *testing.T) { 131 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 132 | m := mock.New(t) 133 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 134 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 135 | applier := makeApplier(t, m, createConfigs()) 136 | hook := createHook() 137 | testErr := errors.New("missing file") 138 | m.GetFileErr = testErr 139 | 140 | err := applier.UpdateFromHook(context.Background(), hook) 141 | 142 | if err != testErr { 143 | t.Fatalf("got %s, want %s", err, testErr) 144 | } 145 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 146 | if s := string(updated); s != "" { 147 | t.Fatalf("update failed, got %#v, want %#v", s, "") 148 | } 149 | m.AssertNoBranchesCreated() 150 | m.AssertNoPullRequestsCreated() 151 | } 152 | 153 | func TestUpdaterWithBranchCreationFailure(t *testing.T) { 154 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 155 | m := mock.New(t) 156 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 157 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 158 | applier := makeApplier(t, m, createConfigs()) 159 | hook := createHook() 160 | testErr := errors.New("can't create branch") 161 | m.CreateBranchErr = testErr 162 | 163 | err := applier.UpdateFromHook(context.Background(), hook) 164 | 165 | if err.Error() != "failed to create branch: can't create branch" { 166 | t.Fatalf("got %s, want %s", err, "failed to create branch: can't create branch") 167 | } 168 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 169 | if s := string(updated); s != "" { 170 | t.Fatalf("update failed, got %#v, want %#v", s, "") 171 | } 172 | m.AssertNoBranchesCreated() 173 | m.AssertNoPullRequestsCreated() 174 | } 175 | 176 | func TestUpdaterWithUpdateFileFailure(t *testing.T) { 177 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 178 | m := mock.New(t) 179 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 180 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 181 | applier := makeApplier(t, m, createConfigs()) 182 | hook := createHook() 183 | testErr := errors.New("can't update file") 184 | m.UpdateFileErr = testErr 185 | 186 | err := applier.UpdateFromHook(context.Background(), hook) 187 | 188 | if err.Error() != "failed to update file: can't update file" { 189 | t.Fatalf("got %s, want %s", err, "failed to update file: can't update file") 190 | } 191 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 192 | if s := string(updated); s != "" { 193 | t.Fatalf("update failed, got %#v, want %#v", s, "") 194 | } 195 | m.AssertBranchCreated(testGitHubRepo, "test-branch-a", testSHA) 196 | m.RefutePullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 197 | Title: fmt.Sprintf("Image %s updated", testQuayRepo), 198 | Body: "Automated Image Update", 199 | Head: "test-branch-a", 200 | Base: "master", 201 | }) 202 | } 203 | 204 | func TestUpdaterWithCreatePullRequestFailure(t *testing.T) { 205 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 206 | m := mock.New(t) 207 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 208 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 209 | applier := makeApplier(t, m, createConfigs()) 210 | hook := createHook() 211 | testErr := errors.New("failure") 212 | m.CreatePullRequestErr = testErr 213 | 214 | err := applier.UpdateFromHook(context.Background(), hook) 215 | 216 | if err.Error() != "failed to create pull request in repo testorg/testrepo: failed to create a pull request: failure" { 217 | t.Fatalf("got %s, want %s", err, "failed to create a pull request: can't create pull-request") 218 | } 219 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 220 | want := "test:\n image: quay.io/testorg/repo:production\n" 221 | if s := string(updated); s != want { 222 | t.Fatalf("update failed, got %#v, want %#v", s, "") 223 | } 224 | m.AssertBranchCreated(testGitHubRepo, "test-branch-a", testSHA) 225 | m.RefutePullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 226 | Title: fmt.Sprintf("Image %s updated", testQuayRepo), 227 | Body: "Automated Image Update", 228 | Head: "test-branch-a", 229 | Base: "master", 230 | }) 231 | } 232 | 233 | func TestUpdaterWithNonMasterSourceBranch(t *testing.T) { 234 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 235 | m := mock.New(t) 236 | m.AddFileContents(testGitHubRepo, testFilePath, "staging", []byte("test:\n image: old-image\n")) 237 | m.AddBranchHead(testGitHubRepo, "staging", testSHA) 238 | configs := createConfigs() 239 | configs.Repositories[0].SourceBranch = "staging" 240 | applier := makeApplier(t, m, configs) 241 | hook := createHook() 242 | 243 | err := applier.UpdateFromHook(context.Background(), hook) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | 248 | updated := m.GetUpdatedContents(testGitHubRepo, testFilePath, "test-branch-a") 249 | want := "test:\n image: quay.io/testorg/repo:production\n" 250 | if s := string(updated); s != want { 251 | t.Fatalf("update failed, got %#v, want %#v", s, want) 252 | } 253 | m.AssertBranchCreated(testGitHubRepo, "test-branch-a", testSHA) 254 | m.AssertPullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 255 | Title: "Automated image update", 256 | Body: fmt.Sprintf("Automated update from %q", testQuayRepo), 257 | Head: "test-branch-a", 258 | Base: "staging", 259 | }) 260 | } 261 | 262 | func makeApplier(t *testing.T, m *mock.MockClient, cfgs *config.RepoConfiguration) *Applier { 263 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 264 | applier := New(logger, m, cfgs, updater.NameGenerator(stubNameGenerator{name: "a"})) 265 | return applier 266 | } 267 | 268 | func createHook() *quay.RepositoryPushHook { 269 | return &quay.RepositoryPushHook{ 270 | Repository: testQuayRepo, 271 | DockerURL: "quay.io/testorg/repo", 272 | UpdatedTags: []string{"production"}, 273 | } 274 | } 275 | 276 | func createConfigs() *config.RepoConfiguration { 277 | return &config.RepoConfiguration{ 278 | Repositories: []*config.Repository{ 279 | { 280 | Name: testQuayRepo, 281 | SourceRepo: testGitHubRepo, 282 | SourceBranch: "master", 283 | FilePath: testFilePath, 284 | UpdateKey: "test.image", 285 | BranchGenerateName: "test-branch-", 286 | }, 287 | }, 288 | } 289 | } 290 | 291 | type stubNameGenerator struct { 292 | name string 293 | } 294 | 295 | func (s stubNameGenerator) PrefixedName(p string) string { 296 | return p + s.name 297 | } 298 | -------------------------------------------------------------------------------- /pkg/cmd/client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | 7 | "github.com/jenkins-x/go-scm/scm" 8 | "github.com/jenkins-x/go-scm/scm/factory" 9 | "github.com/spf13/viper" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func createClientFromViper() (*scm.Client, error) { 14 | authToken := viper.GetString(authTokenFlag) 15 | driver := viper.GetString(driverFlag) 16 | apiEndpoint := viper.GetString(apiEndpointFlag) 17 | if viper.GetBool(insecureFlag) { 18 | return factory.NewClient( 19 | driver, 20 | apiEndpoint, 21 | "", 22 | factory.Client(makeInsecureClient(authToken))) 23 | 24 | } 25 | return factory.NewClient( 26 | driver, 27 | apiEndpoint, 28 | authToken) 29 | } 30 | 31 | func makeInsecureClient(token string) *http.Client { 32 | ts := oauth2.StaticTokenSource( 33 | &oauth2.Token{AccessToken: token}, 34 | ) 35 | return &http.Client{ 36 | Transport: &oauth2.Transport{ 37 | Source: ts, 38 | Base: &http.Transport{ 39 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 40 | }, 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/cmd/http.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/go-logr/zapr" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "go.uber.org/zap" 12 | 13 | "github.com/gitops-tools/image-updater/pkg/applier" 14 | "github.com/gitops-tools/image-updater/pkg/config" 15 | "github.com/gitops-tools/image-updater/pkg/handler" 16 | "github.com/gitops-tools/image-updater/pkg/hooks" 17 | "github.com/gitops-tools/image-updater/pkg/hooks/docker" 18 | "github.com/gitops-tools/image-updater/pkg/hooks/quay" 19 | "github.com/gitops-tools/pkg/client" 20 | ) 21 | 22 | func makeHTTPCmd() *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "http", 25 | Short: "update repositories in response to image hooks", 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | zapl, _ := zap.NewProduction() 28 | defer func() { 29 | _ = zapl.Sync() // flushes buffer, if any 30 | }() 31 | logger := zapr.NewLogger(zapl) 32 | scmClient, err := createClientFromViper() 33 | if err != nil { 34 | return fmt.Errorf("failed to create a git driver: %s", err) 35 | } 36 | f, err := os.Open(viper.GetString("config")) 37 | if err != nil { 38 | return err 39 | } 40 | defer f.Close() 41 | repos, err := config.Parse(f) 42 | if err != nil { 43 | return err 44 | } 45 | applier := applier.New(logger, client.New(scmClient), repos) 46 | p, err := parser() 47 | if err != nil { 48 | return err 49 | } 50 | handler := handler.New(logger, applier, p) 51 | http.Handle("/", handler) 52 | listen := fmt.Sprintf(":%d", viper.GetInt("port")) 53 | logger.Info("quay-hooks http starting", "port", viper.GetInt("port"), "parser", viper.GetString("parser")) 54 | return http.ListenAndServe(listen, nil) 55 | }, 56 | } 57 | 58 | cmd.Flags().Int( 59 | "port", 60 | 8080, 61 | "port to serve requests on", 62 | ) 63 | logIfError(viper.BindPFlag("port", cmd.Flags().Lookup("port"))) 64 | 65 | cmd.Flags().String( 66 | "parser", 67 | "quay", 68 | "what driver to use to parse incoming webhooks e.g. quay, docker", 69 | ) 70 | logIfError(viper.BindPFlag("parser", cmd.Flags().Lookup("parser"))) 71 | 72 | cmd.Flags().String( 73 | "config", 74 | "/etc/image-updater/config.yaml", 75 | "repository configuration", 76 | ) 77 | logIfError(viper.BindPFlag("config", cmd.Flags().Lookup("config"))) 78 | 79 | return cmd 80 | } 81 | 82 | func parser() (hooks.PushEventParser, error) { 83 | switch viper.GetString("parser") { 84 | case "quay": 85 | return quay.Parse, nil 86 | case "docker": 87 | return docker.Parse, nil 88 | default: 89 | return nil, fmt.Errorf("unknown parser: %s", viper.GetString("parser")) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/cmd/pubsub.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "cloud.google.com/go/pubsub" 9 | "github.com/gitops-tools/image-updater/pkg/applier" 10 | "github.com/gitops-tools/image-updater/pkg/config" 11 | "github.com/gitops-tools/image-updater/pkg/hooks/gcr" 12 | "github.com/gitops-tools/image-updater/pkg/pubsubhandler" 13 | "github.com/gitops-tools/pkg/client" 14 | "github.com/go-logr/zapr" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | const ( 21 | projectIDFlag = "project-id" 22 | subscriptionNameFlag = "subscription-name" 23 | ) 24 | 25 | type message struct { 26 | data []byte 27 | } 28 | 29 | func (m *message) Ack() {} 30 | func (m *message) Data() []byte { return m.data } 31 | 32 | func makePubsubCmd() *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "pubsub", 35 | Short: "update repositories in response to gcr pubsub events", 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | zapl, _ := zap.NewProduction() 38 | defer func() { 39 | _ = zapl.Sync() // flushes buffer, if any 40 | }() 41 | logger := zapr.NewLogger(zapl) 42 | scmClient, err := createClientFromViper() 43 | if err != nil { 44 | return fmt.Errorf("failed to create a git driver: %s", err) 45 | } 46 | f, err := os.Open(viper.GetString("config")) 47 | if err != nil { 48 | return err 49 | } 50 | defer f.Close() 51 | repos, err := config.Parse(f) 52 | if err != nil { 53 | return err 54 | } 55 | applier := applier.New(logger, client.New(scmClient), repos) 56 | 57 | sub, err := createSubscriptionFromViper() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | handler := pubsubhandler.New(logger, applier, gcr.Parse) 63 | 64 | return sub.Receive(context.Background(), func(ctx context.Context, msg *pubsub.Message) { 65 | handler.Handle(ctx, &message{ 66 | data: msg.Data, 67 | }) 68 | }) 69 | }, 70 | } 71 | 72 | cmd.Flags().String( 73 | "config", 74 | "/etc/image-updater/config.yaml", 75 | "repository configuration", 76 | ) 77 | logIfError(viper.BindPFlag("config", cmd.Flags().Lookup("config"))) 78 | 79 | cmd.Flags().String( 80 | projectIDFlag, 81 | "", 82 | "GCP project ID", 83 | ) 84 | logIfError(viper.BindPFlag(projectIDFlag, cmd.Flags().Lookup(projectIDFlag))) 85 | logIfError(cmd.MarkFlagRequired(projectIDFlag)) 86 | 87 | cmd.Flags().String( 88 | subscriptionNameFlag, 89 | "", 90 | "GCP subscription name", 91 | ) 92 | logIfError(viper.BindPFlag(subscriptionNameFlag, cmd.Flags().Lookup(subscriptionNameFlag))) 93 | logIfError(cmd.MarkFlagRequired(subscriptionNameFlag)) 94 | 95 | return cmd 96 | } 97 | 98 | func createSubscriptionFromViper() (*pubsub.Subscription, error) { 99 | ctx := context.Background() 100 | projectID := viper.GetString(projectIDFlag) 101 | subscriptionName := viper.GetString(subscriptionNameFlag) 102 | 103 | client, err := pubsub.NewClient(ctx, projectID) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | sub := client.Subscription(subscriptionName) 109 | return sub, nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | const ( 11 | driverFlag = "driver" 12 | apiEndpointFlag = "api-endpoint" 13 | authTokenFlag = "auth_token" 14 | insecureFlag = "insecure" 15 | ) 16 | 17 | func init() { 18 | cobra.OnInitialize(initConfig) 19 | } 20 | 21 | func logIfError(e error) { 22 | if e != nil { 23 | log.Fatal(e) 24 | } 25 | } 26 | 27 | func makeRootCmd() *cobra.Command { 28 | cmd := &cobra.Command{ 29 | Use: "image-updater", 30 | TraverseChildren: true, 31 | Short: "Update YAML files in a Git service, with optional automated Pull Requests", 32 | } 33 | 34 | cmd.PersistentFlags().String( 35 | driverFlag, 36 | "github", 37 | "go-scm driver name to use e.g. github, gitlab", 38 | ) 39 | logIfError(viper.BindPFlag(driverFlag, cmd.PersistentFlags().Lookup(driverFlag))) 40 | cmd.PersistentFlags().String( 41 | authTokenFlag, 42 | "", 43 | "The token to authenticate requests to your Git service", 44 | ) 45 | logIfError(viper.BindPFlag(authTokenFlag, cmd.PersistentFlags().Lookup(authTokenFlag))) 46 | 47 | cmd.PersistentFlags().String( 48 | apiEndpointFlag, 49 | "", 50 | "The API endpoint to communicate with private GitLab/GitHub installations", 51 | ) 52 | logIfError(viper.BindPFlag(apiEndpointFlag, cmd.PersistentFlags().Lookup(apiEndpointFlag))) 53 | 54 | cmd.PersistentFlags().Bool( 55 | insecureFlag, 56 | false, 57 | "Allow insecure server connections when using SSL", 58 | ) 59 | logIfError(viper.BindPFlag(insecureFlag, cmd.PersistentFlags().Lookup(insecureFlag))) 60 | 61 | cmd.AddCommand(makeHTTPCmd()) 62 | cmd.AddCommand(makeUpdateCmd()) 63 | cmd.AddCommand(makePubsubCmd()) 64 | return cmd 65 | } 66 | 67 | func initConfig() { 68 | viper.AutomaticEnv() 69 | } 70 | 71 | // Execute is the main entry point into this component. 72 | func Execute() { 73 | if err := makeRootCmd().Execute(); err != nil { 74 | log.Fatal(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pkg/cmd/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-logr/zapr" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "go.uber.org/zap" 11 | 12 | "github.com/gitops-tools/image-updater/pkg/applier" 13 | "github.com/gitops-tools/image-updater/pkg/config" 14 | "github.com/gitops-tools/pkg/client" 15 | ) 16 | 17 | func makeUpdateCmd() *cobra.Command { 18 | cmd := &cobra.Command{ 19 | Use: "update", 20 | Short: "update a repository configuration", 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | logger, _ := zap.NewProduction() 23 | defer func() { 24 | _ = logger.Sync() // flushes buffer, if any 25 | }() 26 | scmClient, err := createClientFromViper() 27 | if err != nil { 28 | return fmt.Errorf("failed to create a git driver: %s", err) 29 | } 30 | applier := applier.New(zapr.NewLogger(logger), client.New(scmClient), nil) 31 | return applier.UpdateRepository(context.Background(), configFromFlags(), viper.GetString("new-image-url")) 32 | }, 33 | } 34 | 35 | cmd.Flags().String( 36 | "new-image-url", 37 | "", 38 | "Image URL to populate the file with e.g. myorg/my-image", 39 | ) 40 | logIfError(viper.BindPFlag("new-image-url", cmd.Flags().Lookup("new-image-url"))) 41 | logIfError(cmd.MarkFlagRequired("new-image-url")) 42 | 43 | addConfigFlags(cmd) 44 | 45 | return cmd 46 | } 47 | 48 | func addConfigFlags(cmd *cobra.Command) { 49 | cmd.Flags().String( 50 | "image-repo", 51 | "", 52 | "Image repo e.g. org/repo that is being updated - used in the created PR", 53 | ) 54 | logIfError(viper.BindPFlag("image-repo", cmd.Flags().Lookup("image-repo"))) 55 | logIfError(cmd.MarkFlagRequired("image-repo")) 56 | 57 | cmd.Flags().String( 58 | "source-repo", 59 | "", 60 | "Git repository to update e.g. org/repo", 61 | ) 62 | logIfError(viper.BindPFlag("source-repo", cmd.Flags().Lookup("source-repo"))) 63 | logIfError(cmd.MarkFlagRequired("source-repo")) 64 | 65 | cmd.Flags().String( 66 | "source-branch", 67 | "master", 68 | "Branch to fetch for updating", 69 | ) 70 | logIfError(viper.BindPFlag("source-branch", cmd.Flags().Lookup("source-branch"))) 71 | 72 | cmd.Flags().String( 73 | "file-path", 74 | "", 75 | "Path within the source-repo to update", 76 | ) 77 | logIfError(viper.BindPFlag("file-path", cmd.Flags().Lookup("file-path"))) 78 | logIfError(cmd.MarkFlagRequired("file-path")) 79 | 80 | cmd.Flags().String( 81 | "update-key", 82 | "", 83 | "JSON path within the file-path to update e.g. spec.template.spec.containers.0.image", 84 | ) 85 | logIfError(viper.BindPFlag("update-key", cmd.Flags().Lookup("update-key"))) 86 | logIfError(cmd.MarkFlagRequired("update-key")) 87 | 88 | cmd.Flags().String( 89 | "branch-generate-name", 90 | "", 91 | "Prefix for naming automatically generated branch, if empty, this will update source-branch", 92 | ) 93 | logIfError(viper.BindPFlag("branch-generate-name", cmd.Flags().Lookup("branch-generate-name"))) 94 | } 95 | 96 | func configFromFlags() *config.Repository { 97 | return &config.Repository{ 98 | Name: viper.GetString("image-repo"), 99 | SourceRepo: viper.GetString("source-repo"), 100 | SourceBranch: viper.GetString("source-branch"), 101 | FilePath: viper.GetString("file-path"), 102 | UpdateKey: viper.GetString("update-key"), 103 | BranchGenerateName: viper.GetString("branch-generate-name"), 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | // Repository is the items that are required to update a specific file in a repo. 12 | type Repository struct { 13 | Name string `json:"name"` 14 | SourceRepo string `json:"sourceRepo"` 15 | SourceBranch string `json:"sourceBranch"` 16 | FilePath string `json:"filePath"` 17 | UpdateKey string `json:"updateKey"` 18 | BranchGenerateName string `json:"branchGenerateName"` 19 | TagMatch string `json:"tagMatch"` 20 | } 21 | 22 | // Parse reads and returns a configuration from Reader. 23 | func Parse(in io.Reader) (*RepoConfiguration, error) { 24 | body, err := ioutil.ReadAll(in) 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to read YAML: %w", err) 27 | } 28 | rc := &RepoConfiguration{} 29 | err = yaml.Unmarshal(body, rc) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) 32 | } 33 | return rc, nil 34 | } 35 | 36 | // RepoConfiguration is a slice of Repository values. 37 | type RepoConfiguration struct { 38 | Repositories []*Repository `json:"repositories"` 39 | } 40 | 41 | // Find looks up the repository by name. 42 | func (c RepoConfiguration) Find(name string) *Repository { 43 | for _, cfg := range c.Repositories { 44 | if cfg.Name == name { 45 | return cfg 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestRepoConfigurationFind(t *testing.T) { 12 | findTests := []struct { 13 | name string 14 | want *Repository 15 | }{ 16 | {"testing", &Repository{Name: "testing"}}, 17 | {"unknown", nil}, 18 | } 19 | 20 | cfgs := RepoConfiguration{ 21 | Repositories: []*Repository{ 22 | {Name: "testing"}, 23 | {Name: "another"}, 24 | }, 25 | } 26 | 27 | for _, tt := range findTests { 28 | if diff := cmp.Diff(tt.want, cfgs.Find(tt.name)); diff != "" { 29 | t.Errorf("Find(%s) failed:\n %s", tt.name, diff) 30 | } 31 | } 32 | } 33 | 34 | func TestParse(t *testing.T) { 35 | parseTests := []struct { 36 | filename string 37 | want *RepoConfiguration 38 | }{ 39 | { 40 | "testdata/config.yaml", &RepoConfiguration{ 41 | Repositories: []*Repository{ 42 | { 43 | Name: "testing/repo-image", 44 | SourceRepo: "example/example-source", 45 | SourceBranch: "main", 46 | FilePath: "test/file.yaml", 47 | UpdateKey: "person.name", 48 | BranchGenerateName: "repo-imager-", 49 | TagMatch: ".*main", 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | 56 | for _, tt := range parseTests { 57 | t.Run(fmt.Sprintf("parsing %s", tt.filename), func(rt *testing.T) { 58 | f, err := os.Open(tt.filename) 59 | if err != nil { 60 | rt.Errorf("failed to open %v: %s", tt.filename, err) 61 | } 62 | defer f.Close() 63 | 64 | got, err := Parse(f) 65 | if err != nil { 66 | rt.Errorf("failed to parse %v: %s", tt.filename, err) 67 | return 68 | } 69 | if diff := cmp.Diff(tt.want, got); diff != "" { 70 | rt.Errorf("Parse(%s) failed diff\n%s", tt.filename, diff) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/config/testdata/config.yaml: -------------------------------------------------------------------------------- 1 | repositories: 2 | - name: testing/repo-image 3 | sourceRepo: example/example-source 4 | sourceBranch: main 5 | filePath: test/file.yaml 6 | updateKey: person.name 7 | branchGenerateName: repo-imager- 8 | tagMatch: ".*main" 9 | -------------------------------------------------------------------------------- /pkg/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | 7 | "github.com/go-logr/logr" 8 | 9 | "github.com/gitops-tools/image-updater/pkg/applier" 10 | "github.com/gitops-tools/image-updater/pkg/hooks" 11 | ) 12 | 13 | // Handler parses and processes hook notifications. 14 | type Handler struct { 15 | log logr.Logger 16 | applier *applier.Applier 17 | parser hooks.PushEventParser 18 | } 19 | 20 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 21 | hook, err := h.parse(r) 22 | if err != nil { 23 | h.log.Error(err, "failed to parse request") 24 | http.Error(w, err.Error(), http.StatusInternalServerError) 25 | return 26 | } 27 | err = h.applier.UpdateFromHook(r.Context(), hook) 28 | 29 | if err != nil { 30 | h.log.Error(err, "hook update failed") 31 | http.Error(w, err.Error(), http.StatusInternalServerError) 32 | return 33 | } 34 | } 35 | 36 | func (h *Handler) parse(r *http.Request) (hooks.PushEvent, error) { 37 | h.log.Info("processing hook request") 38 | // TODO: LimitReader 39 | data, err := ioutil.ReadAll(r.Body) 40 | if err != nil { 41 | h.log.Error(err, "failed to read request body") 42 | return nil, err 43 | } 44 | return h.parser(data) 45 | } 46 | 47 | // New creates and returns a new Handler. 48 | func New(logger logr.Logger, u *applier.Applier, p hooks.PushEventParser) *Handler { 49 | return &Handler{log: logger, applier: u, parser: p} 50 | } 51 | -------------------------------------------------------------------------------- /pkg/handler/handler_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gitops-tools/pkg/client/mock" 13 | "github.com/gitops-tools/pkg/updater" 14 | "github.com/go-logr/zapr" 15 | "github.com/jenkins-x/go-scm/scm" 16 | "go.uber.org/zap" 17 | "go.uber.org/zap/zaptest" 18 | 19 | "github.com/gitops-tools/image-updater/pkg/applier" 20 | "github.com/gitops-tools/image-updater/pkg/config" 21 | "github.com/gitops-tools/image-updater/pkg/hooks" 22 | "github.com/gitops-tools/image-updater/pkg/hooks/quay" 23 | ) 24 | 25 | const ( 26 | testQuayRepo = "mynamespace/repository" 27 | testGitHubRepo = "testorg/testrepo" 28 | testFilePath = "environments/test/services/service-a/test.yaml" 29 | ) 30 | 31 | func TestHandler(t *testing.T) { 32 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 33 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 34 | m := mock.New(t) 35 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 36 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 37 | h := New(logger, applier.New(logger, m, createConfigs(), updater.NameGenerator(stubNameGenerator{"a"})), quay.Parse) 38 | rec := httptest.NewRecorder() 39 | req := makeHookRequest(t, "testdata/push_hook.json") 40 | 41 | h.ServeHTTP(rec, req) 42 | 43 | m.AssertPullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 44 | Body: fmt.Sprintf("Automated update from %q", testQuayRepo), 45 | Head: "test-branch-a", 46 | Base: "master", 47 | Title: "Automated image update", 48 | }) 49 | } 50 | 51 | func TestHandlerWithParseFailure(t *testing.T) { 52 | badParser := func(payload []byte) (hooks.PushEvent, error) { 53 | return nil, errors.New("failed") 54 | } 55 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 56 | m := mock.New(t) 57 | applier := applier.New(logger, m, createConfigs(), updater.NameGenerator(stubNameGenerator{"a"})) 58 | h := New(logger, applier, badParser) 59 | rec := httptest.NewRecorder() 60 | req := makeHookRequest(t, "testdata/push_hook.json") 61 | 62 | h.ServeHTTP(rec, req) 63 | 64 | m.AssertNoPullRequestsCreated() 65 | res := rec.Result() 66 | if res.StatusCode != http.StatusInternalServerError { 67 | t.Fatalf("StatusCode got %d, want %d", res.StatusCode, http.StatusInternalServerError) 68 | } 69 | } 70 | 71 | func TestHandlerWithFailureToUpdate(t *testing.T) { 72 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 73 | m := mock.New(t) 74 | applier := applier.New(logger, m, createConfigs(), updater.NameGenerator(stubNameGenerator{"a"})) 75 | h := New(logger, applier, quay.Parse) 76 | rec := httptest.NewRecorder() 77 | req := makeHookRequest(t, "testdata/push_hook.json") 78 | 79 | h.ServeHTTP(rec, req) 80 | 81 | m.AssertNoPullRequestsCreated() 82 | res := rec.Result() 83 | if res.StatusCode != http.StatusInternalServerError { 84 | t.Fatalf("StatusCode got %d, want %d", res.StatusCode, http.StatusInternalServerError) 85 | } 86 | } 87 | 88 | func TestParseWithNoBody(t *testing.T) { 89 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 90 | m := mock.New(t) 91 | applier := applier.New(logger, m, createConfigs(), updater.NameGenerator(stubNameGenerator{"a"})) 92 | h := New(logger, applier, quay.Parse) 93 | bodyErr := errors.New("just a test error") 94 | 95 | req := httptest.NewRequest("POST", "/", failingReader{err: bodyErr}) 96 | 97 | _, err := h.parse(req) 98 | if err != bodyErr { 99 | t.Fatal("expected an error") 100 | } 101 | } 102 | 103 | func TestParseWithUnparseableBody(t *testing.T) { 104 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 105 | m := mock.New(t) 106 | applier := applier.New(logger, m, createConfigs(), updater.NameGenerator(stubNameGenerator{"a"})) 107 | h := New(logger, applier, quay.Parse) 108 | 109 | req := httptest.NewRequest("POST", "/", nil) 110 | 111 | _, err := h.parse(req) 112 | 113 | if err == nil { 114 | t.Fatal("expected an error") 115 | } 116 | } 117 | 118 | func makeHookRequest(t *testing.T, fixture string) *http.Request { 119 | t.Helper() 120 | b, err := ioutil.ReadFile(fixture) 121 | if err != nil { 122 | t.Fatalf("failed to read %s: %s", fixture, err) 123 | } 124 | req := httptest.NewRequest("POST", "/", bytes.NewReader(b)) 125 | req.Header.Add("Content-Type", "application/json") 126 | return req 127 | } 128 | 129 | func createConfigs() *config.RepoConfiguration { 130 | return &config.RepoConfiguration{ 131 | Repositories: []*config.Repository{ 132 | { 133 | Name: testQuayRepo, 134 | SourceRepo: testGitHubRepo, 135 | SourceBranch: "master", 136 | FilePath: testFilePath, 137 | UpdateKey: "test.image", 138 | BranchGenerateName: "test-branch-", 139 | }, 140 | }, 141 | } 142 | } 143 | 144 | type stubNameGenerator struct { 145 | name string 146 | } 147 | 148 | func (s stubNameGenerator) PrefixedName(p string) string { 149 | return p + s.name 150 | } 151 | 152 | type failingReader struct { 153 | err error 154 | } 155 | 156 | func (f failingReader) Read(p []byte) (n int, err error) { 157 | return 0, f.err 158 | } 159 | func (f failingReader) Close() error { 160 | return f.err 161 | } 162 | -------------------------------------------------------------------------------- /pkg/handler/testdata/push_hook.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repository", 3 | "repository": "mynamespace/repository", 4 | "namespace": "mynamespace", 5 | "docker_url": "quay.io/mynamespace/repository", 6 | "homepage": "https://quay.io/repository/mynamespace/repository", 7 | "updated_tags": [ 8 | "latest" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /pkg/hooks/docker/hook.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/gitops-tools/image-updater/pkg/hooks" 8 | ) 9 | 10 | // Parse parses a payload into a Docker webhook event. 11 | func Parse(payload []byte) (hooks.PushEvent, error) { 12 | h := &Webhook{} 13 | err := json.Unmarshal(payload, h) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return h, nil 18 | } 19 | 20 | // Webhook is a struct for the Docker Hub webhook event. 21 | type Webhook struct { 22 | CallbackURL string `json:"callback_url"` 23 | PushData *PushData `json:"push_data"` 24 | Repository *Repository `json:"repository"` 25 | } 26 | 27 | // PushedImageURL is an implementation of the hooks.PushEvent interface. 28 | func (p Webhook) PushedImageURL() string { 29 | return fmt.Sprintf("%s:%s", p.Repository.RepoName, p.PushData.Tag) 30 | } 31 | 32 | // EventRepository is an implementation of the hooks.PushEvent interface. 33 | func (p Webhook) EventRepository() string { 34 | return p.Repository.RepoName 35 | } 36 | 37 | // EventTag is an implementation of the hooks.PushEvent interface. 38 | func (p Webhook) EventTag() string { 39 | return p.PushData.Tag 40 | } 41 | 42 | // PushData is part of the Webhook struct. 43 | type PushData struct { 44 | Images []string `json:"images"` 45 | PushedAt float64 `json:"pushed_at"` 46 | Pusher string `json:"pusher"` 47 | Tag string `json:"tag"` 48 | } 49 | 50 | // Repository is part of the Webhook struct. 51 | type Repository struct { 52 | RepoName string `json:"repo_name"` 53 | Name string `json:"name"` 54 | Namespace string `json:"namespace"` 55 | Owner string `json:"owner"` 56 | Description string `json:"description"` 57 | FullDescription string `json:"full_description"` 58 | RepoURL string `json:"repo_url"` 59 | Dockerfile string `json:"dockerfile"` 60 | Status string `json:"status"` 61 | IsOfficial bool `json:"is_official"` 62 | IsPrivate bool `json:"is_private"` 63 | IsTrusted bool `json:"is_trusted"` 64 | DateCreated float64 `json:"date_created"` 65 | StarCount int64 `json:"star_count"` 66 | CommentCount int64 `json:"comment_count"` 67 | } 68 | -------------------------------------------------------------------------------- /pkg/hooks/docker/hook_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/gitops-tools/image-updater/pkg/hooks" 10 | ) 11 | 12 | var _ hooks.PushEvent = (*Webhook)(nil) 13 | var _ hooks.PushEventParser = Parse 14 | 15 | func TestParse(t *testing.T) { 16 | req := readFixture(t, "testdata/push_event.json") 17 | 18 | hook, err := Parse(req) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | want := &Webhook{ 24 | CallbackURL: "https://registry.hub.docker.com/u/svendowideit/testhook/hook/2141b5bi5i5b02bec211i4eeih0242eg11000a/", 25 | PushData: &PushData{ 26 | Pusher: "trustedbuilder", 27 | Tag: "latest", 28 | Images: []string{ 29 | "27d47432a69bca5f2700e4dff7de0388ed65f9d3fb1ec645e2bc24c223dc1cc3", 30 | "51a9c7c1f8bb2fa19bcd09789a34e63f35abb80044bc10196e304f6634cc582c", 31 | "...", 32 | }, 33 | PushedAt: 1.417566161e+09, 34 | }, 35 | Repository: &Repository{ 36 | RepoName: "svendowideit/testhook", 37 | Name: "testhook", 38 | Namespace: "svendowideit", 39 | Owner: "svendowideit", 40 | FullDescription: "Docker Hub based automated build from a GitHub repo", 41 | RepoURL: "https://registry.hub.docker.com/u/svendowideit/testhook/", 42 | Dockerfile: "#\n# BUILD\t\tdocker build -t svendowideit/apt-cacher .\n# RUN\t\tdocker run -d -p 3142:3142 -name apt-cacher-run apt-cacher\n#\n# and then you can run containers with:\n# \t\tdocker run -t -i -rm -e http_proxy http://192.168.1.2:3142/ debian bash\n#\nFROM\t\tubuntu\n\n\nVOLUME\t\t[/var/cache/apt-cacher-ng]\nRUN\t\tapt-get update ; apt-get install -yq apt-cacher-ng\n\nEXPOSE \t\t3142\nCMD\t\tchmod 777 /var/cache/apt-cacher-ng ; /etc/init.d/apt-cacher-ng start ; tail -f /var/log/apt-cacher-ng/*\n", 43 | Status: "Active", 44 | IsPrivate: true, 45 | IsTrusted: true, 46 | DateCreated: 1.417494799e+09, 47 | }, 48 | } 49 | if diff := cmp.Diff(want, hook); diff != "" { 50 | t.Fatalf("hook doesn't match:\n%s", diff) 51 | } 52 | } 53 | 54 | func TestPushedImageURL(t *testing.T) { 55 | hook := &Webhook{ 56 | PushData: &PushData{ 57 | Tag: "latest", 58 | }, 59 | Repository: &Repository{ 60 | RepoName: "mynamespace/repository", 61 | }, 62 | } 63 | want := "mynamespace/repository:latest" 64 | 65 | if u := hook.PushedImageURL(); u != want { 66 | t.Fatalf("got %s, want %s", u, want) 67 | } 68 | } 69 | 70 | func TestRepository(t *testing.T) { 71 | hook := &Webhook{ 72 | PushData: &PushData{ 73 | Tag: "latest", 74 | }, 75 | Repository: &Repository{ 76 | RepoName: "mynamespace/repository", 77 | }, 78 | } 79 | want := "mynamespace/repository" 80 | 81 | if u := hook.EventRepository(); u != want { 82 | t.Fatalf("got %s, want %s", u, want) 83 | } 84 | } 85 | 86 | func readFixture(t *testing.T, fixture string) []byte { 87 | t.Helper() 88 | b, err := ioutil.ReadFile(fixture) 89 | if err != nil { 90 | t.Fatalf("failed to read %s: %s", fixture, err) 91 | } 92 | return b 93 | } 94 | -------------------------------------------------------------------------------- /pkg/hooks/docker/testdata/push_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "callback_url": "https://registry.hub.docker.com/u/svendowideit/testhook/hook/2141b5bi5i5b02bec211i4eeih0242eg11000a/", 3 | "push_data": { 4 | "images": [ 5 | "27d47432a69bca5f2700e4dff7de0388ed65f9d3fb1ec645e2bc24c223dc1cc3", 6 | "51a9c7c1f8bb2fa19bcd09789a34e63f35abb80044bc10196e304f6634cc582c", 7 | "..." 8 | ], 9 | "pushed_at": 1.417566161e+09, 10 | "pusher": "trustedbuilder", 11 | "tag": "latest" 12 | }, 13 | "repository": { 14 | "comment_count": 0, 15 | "date_created": 1.417494799e+09, 16 | "description": "", 17 | "dockerfile": "#\n# BUILD\u0009\u0009docker build -t svendowideit/apt-cacher .\n# RUN\u0009\u0009docker run -d -p 3142:3142 -name apt-cacher-run apt-cacher\n#\n# and then you can run containers with:\n# \u0009\u0009docker run -t -i -rm -e http_proxy http://192.168.1.2:3142/ debian bash\n#\nFROM\u0009\u0009ubuntu\n\n\nVOLUME\u0009\u0009[/var/cache/apt-cacher-ng]\nRUN\u0009\u0009apt-get update ; apt-get install -yq apt-cacher-ng\n\nEXPOSE \u0009\u00093142\nCMD\u0009\u0009chmod 777 /var/cache/apt-cacher-ng ; /etc/init.d/apt-cacher-ng start ; tail -f /var/log/apt-cacher-ng/*\n", 18 | "full_description": "Docker Hub based automated build from a GitHub repo", 19 | "is_official": false, 20 | "is_private": true, 21 | "is_trusted": true, 22 | "name": "testhook", 23 | "namespace": "svendowideit", 24 | "owner": "svendowideit", 25 | "repo_name": "svendowideit/testhook", 26 | "repo_url": "https://registry.hub.docker.com/u/svendowideit/testhook/", 27 | "star_count": 0, 28 | "status": "Active" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/hooks/gcr/hook.go: -------------------------------------------------------------------------------- 1 | package gcr 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/gitops-tools/image-updater/pkg/hooks" 9 | ) 10 | 11 | // PushMessage is a struct for the GCR push event 12 | type PushMessage struct { 13 | Action string `json:"action,omitempty"` 14 | Digest string `json:"digest,omitempty"` 15 | Tag string `json:"tag,omitempty"` 16 | } 17 | 18 | // PushedImageURL is an implementation of the hooks.PushEvent interface. 19 | func (m PushMessage) PushedImageURL() string { 20 | return m.Tag 21 | } 22 | 23 | // EventRepository is an implementation of the hooks.PushEvent interface. 24 | func (m PushMessage) EventRepository() string { 25 | return strings.Split(m.Tag, ":")[0] 26 | } 27 | 28 | // EventTag is an implementation of the hooks.PushEvent interface. 29 | func (m PushMessage) EventTag() string { 30 | return strings.Split(m.Tag, ":")[1] 31 | } 32 | 33 | // Parse parses a payload into a GCR PushEvent 34 | func Parse(payload []byte) (hooks.PushEvent, error) { 35 | msg := &PushMessage{} 36 | 37 | err := json.Unmarshal(payload, &msg) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if msg.Tag == "" { 43 | return nil, errors.New("tag is empty") 44 | } 45 | 46 | return msg, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/hooks/gcr/hook_test.go: -------------------------------------------------------------------------------- 1 | package gcr 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/gitops-tools/image-updater/pkg/hooks" 10 | ) 11 | 12 | var _ hooks.PushEvent = (*PushMessage)(nil) 13 | var _ hooks.PushEventParser = Parse 14 | 15 | func TestParse(t *testing.T) { 16 | event := readFixture(t, "testdata/push_event.json") 17 | 18 | hook, err := Parse(event) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | want := &PushMessage{ 24 | Action: "INSERT", 25 | Digest: "gcr.io/mynamespace/repository@sha256:6ec128e26cd5", 26 | Tag: "gcr.io/mynamespace/repository:latest", 27 | } 28 | if diff := cmp.Diff(want, hook); diff != "" { 29 | t.Fatalf("hook doesn't match:\n%s", diff) 30 | } 31 | } 32 | 33 | func TestPushedImageURL(t *testing.T) { 34 | hook := &PushMessage{ 35 | Action: "INSERT", 36 | Digest: "gcr.io/mynamespace/repository@sha256:6ec128e26cd5", 37 | Tag: "gcr.io/mynamespace/repository:latest", 38 | } 39 | want := "gcr.io/mynamespace/repository:latest" 40 | 41 | if u := hook.PushedImageURL(); u != want { 42 | t.Fatalf("got %s, want %s", u, want) 43 | } 44 | } 45 | 46 | func TestRepository(t *testing.T) { 47 | hook := &PushMessage{ 48 | Action: "INSERT", 49 | Digest: "gcr.io/mynamespace/repository@sha256:6ec128e26cd5", 50 | Tag: "gcr.io/mynamespace/repository:latest", 51 | } 52 | want := "gcr.io/mynamespace/repository" 53 | 54 | if u := hook.EventRepository(); u != want { 55 | t.Fatalf("got %s, want %s", u, want) 56 | } 57 | } 58 | 59 | func readFixture(t *testing.T, fixture string) []byte { 60 | t.Helper() 61 | b, err := ioutil.ReadFile(fixture) 62 | if err != nil { 63 | t.Fatalf("failed to read %s: %s", fixture, err) 64 | } 65 | return b 66 | } 67 | -------------------------------------------------------------------------------- /pkg/hooks/gcr/testdata/push_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "INSERT", 3 | "digest": "gcr.io/mynamespace/repository@sha256:6ec128e26cd5", 4 | "tag": "gcr.io/mynamespace/repository:latest" 5 | } -------------------------------------------------------------------------------- /pkg/hooks/interface.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | // PushEvent values return the image that is to be inserted into the file. 4 | type PushEvent interface { 5 | PushedImageURL() string 6 | EventRepository() string 7 | EventTag() string 8 | } 9 | 10 | // PushEventParser parses the specifics of a hook request into a body. 11 | type PushEventParser func(payload []byte) (PushEvent, error) 12 | -------------------------------------------------------------------------------- /pkg/hooks/quay/hook.go: -------------------------------------------------------------------------------- 1 | package quay 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/gitops-tools/image-updater/pkg/hooks" 8 | ) 9 | 10 | // Parse parses a payload it into a Quay.io Push hook if possible. 11 | func Parse(payload []byte) (hooks.PushEvent, error) { 12 | h := &RepositoryPushHook{} 13 | err := json.Unmarshal(payload, h) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return h, nil 18 | } 19 | 20 | // RepositoryPushHook is a struct for the Quay.io push event. 21 | type RepositoryPushHook struct { 22 | Name string `json:"name"` 23 | Repository string `json:"repository"` 24 | Namespace string `json:"namespace"` 25 | DockerURL string `json:"docker_url"` 26 | Homepage string `json:"homepage"` 27 | UpdatedTags []string `json:"updated_tags,omitempty"` 28 | } 29 | 30 | // PushedImageURL is an implementation of the hooks.PushEvent interface. 31 | func (p RepositoryPushHook) PushedImageURL() string { 32 | return fmt.Sprintf("%s:%s", p.DockerURL, p.UpdatedTags[0]) 33 | } 34 | 35 | // EventRepository is an implementation of the hooks.PushEvent interface. 36 | func (p RepositoryPushHook) EventRepository() string { 37 | return p.Repository 38 | } 39 | 40 | // EventTag is an implementation of the hooks.PushEvent interface. 41 | func (p RepositoryPushHook) EventTag() string { 42 | return p.UpdatedTags[0] 43 | } 44 | -------------------------------------------------------------------------------- /pkg/hooks/quay/hook_test.go: -------------------------------------------------------------------------------- 1 | package quay 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/gitops-tools/image-updater/pkg/hooks" 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | var _ hooks.PushEvent = (*RepositoryPushHook)(nil) 12 | var _ hooks.PushEventParser = Parse 13 | 14 | func TestParse(t *testing.T) { 15 | req := readFixture(t, "testdata/push_hook.json") 16 | 17 | hook, err := Parse(req) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | want := &RepositoryPushHook{ 23 | Name: "repository", 24 | Repository: "mynamespace/repository", 25 | Namespace: "mynamespace", 26 | DockerURL: "quay.io/mynamespace/repository", 27 | Homepage: "https://quay.io/repository/mynamespace/repository", 28 | UpdatedTags: []string{"latest"}, 29 | } 30 | if diff := cmp.Diff(want, hook); diff != "" { 31 | t.Fatalf("hook doesn't match:\n%s", diff) 32 | } 33 | } 34 | 35 | func TestPushedImageURL(t *testing.T) { 36 | hook := &RepositoryPushHook{ 37 | Name: "repository", 38 | DockerURL: "quay.io/mynamespace/repository", 39 | UpdatedTags: []string{"latest"}, 40 | } 41 | want := "quay.io/mynamespace/repository:latest" 42 | 43 | if u := hook.PushedImageURL(); u != want { 44 | t.Fatalf("got %s, want %s", u, want) 45 | } 46 | } 47 | 48 | func TestEventRepository(t *testing.T) { 49 | hook := &RepositoryPushHook{ 50 | Repository: "mynamespace/repository", 51 | Name: "repository", 52 | DockerURL: "quay.io/mynamespace/repository", 53 | UpdatedTags: []string{"latest"}, 54 | } 55 | want := "mynamespace/repository" 56 | 57 | if u := hook.EventRepository(); u != want { 58 | t.Fatalf("got %s, want %s", u, want) 59 | } 60 | } 61 | 62 | func TestEventTag(t *testing.T) { 63 | hook := &RepositoryPushHook{ 64 | Repository: "mynamespace/repository", 65 | Name: "repository", 66 | DockerURL: "quay.io/mynamespace/repository", 67 | UpdatedTags: []string{"v1", "latest"}, 68 | } 69 | 70 | want := "v1" 71 | if u := hook.EventTag(); u != want { 72 | t.Fatalf("got %s, want %s", u, want) 73 | } 74 | } 75 | 76 | func readFixture(t *testing.T, fixture string) []byte { 77 | t.Helper() 78 | b, err := ioutil.ReadFile(fixture) 79 | if err != nil { 80 | t.Fatalf("failed to read %s: %s", fixture, err) 81 | } 82 | return b 83 | } 84 | -------------------------------------------------------------------------------- /pkg/hooks/quay/testdata/push_hook.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repository", 3 | "repository": "mynamespace/repository", 4 | "namespace": "mynamespace", 5 | "docker_url": "quay.io/mynamespace/repository", 6 | "homepage": "https://quay.io/repository/mynamespace/repository", 7 | "updated_tags": [ 8 | "latest" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /pkg/names/generator.go: -------------------------------------------------------------------------------- 1 | package names 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | ) 7 | 8 | // RandomGenerator generates a random name prefix. 9 | type RandomGenerator struct { 10 | rand *rand.Rand 11 | } 12 | 13 | // New creates and returns a RandomGenerator. 14 | func New(r *rand.Rand) *RandomGenerator { 15 | return &RandomGenerator{rand: r} 16 | } 17 | 18 | // PrefixedName generates a name from the prefix with an additional 5 random 19 | // alphabetic characters. 20 | // TODO: this should limit the length based on the prefix because branch names 21 | // have a limit. 22 | func (g RandomGenerator) PrefixedName(prefix string) string { 23 | charset := "abcdefghijklmnopqrstuvwyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 24 | b := make([]byte, 5) 25 | for i := range b { 26 | b[i] = charset[g.rand.Intn(len(charset))] 27 | } 28 | return fmt.Sprintf("%s%s", prefix, b) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/names/generator_test.go: -------------------------------------------------------------------------------- 1 | package names 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func TestGenerator(t *testing.T) { 9 | g := RandomGenerator{rand: rand.New(rand.NewSource(100))} 10 | 11 | name := g.PrefixedName("testing-") 12 | 13 | if name != "testing-DlPsU" { 14 | t.Fatalf("got %v, want %v", name, "testing-DlPsU") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/names/interface.go: -------------------------------------------------------------------------------- 1 | package names 2 | 3 | // Generator is implemented by values that generate a prefixed-nane. 4 | type Generator interface { 5 | PrefixedName(s string) string 6 | } 7 | -------------------------------------------------------------------------------- /pkg/pubsubhandler/handler.go: -------------------------------------------------------------------------------- 1 | package pubsubhandler 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitops-tools/image-updater/pkg/applier" 7 | "github.com/gitops-tools/image-updater/pkg/hooks" 8 | "github.com/go-logr/logr" 9 | ) 10 | 11 | // Handler parses and processes pubsub messages. 12 | type Handler struct { 13 | applier *applier.Applier 14 | log logr.Logger 15 | parser hooks.PushEventParser 16 | } 17 | 18 | // New creates and returns a new Handler. 19 | func New(logger logr.Logger, u *applier.Applier, p hooks.PushEventParser) *Handler { 20 | return &Handler{log: logger, applier: u, parser: p} 21 | } 22 | 23 | // Handle acks, parses and processes pubsub messages 24 | func (h *Handler) Handle(ctx context.Context, m message) { 25 | h.log.Info("processing hook request") 26 | 27 | hook, err := h.parser(m.Data()) 28 | if err != nil { 29 | h.log.Error(err, "failed to parse request") 30 | return 31 | } 32 | 33 | err = h.applier.UpdateFromHook(ctx, hook) 34 | 35 | if err != nil { 36 | h.log.Error(err, "hook update failed") 37 | return 38 | } 39 | 40 | m.Ack() 41 | } 42 | -------------------------------------------------------------------------------- /pkg/pubsubhandler/handler_test.go: -------------------------------------------------------------------------------- 1 | package pubsubhandler 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/gitops-tools/pkg/client/mock" 10 | "github.com/gitops-tools/pkg/updater" 11 | "github.com/go-logr/zapr" 12 | "github.com/jenkins-x/go-scm/scm" 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zaptest" 15 | 16 | "github.com/gitops-tools/image-updater/pkg/applier" 17 | "github.com/gitops-tools/image-updater/pkg/config" 18 | "github.com/gitops-tools/image-updater/pkg/hooks/gcr" 19 | ) 20 | 21 | const ( 22 | testGcrRepo = "gcr.io/mynamespace/repository" 23 | testGitHubRepo = "testorg/testrepo" 24 | testFilePath = "environments/test/services/service-a/test.yaml" 25 | ) 26 | 27 | func TestHandler(t *testing.T) { 28 | testSHA := "980a0d5f19a64b4b30a87d4206aade58726b60e3" 29 | logger := zapr.NewLogger(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) 30 | m := mock.New(t) 31 | m.AddBranchHead(testGitHubRepo, "master", testSHA) 32 | m.AddFileContents(testGitHubRepo, testFilePath, "master", []byte("test:\n image: old-image\n")) 33 | applier := applier.New(logger, m, createConfigs(), updater.NameGenerator(stubNameGenerator{"a"})) 34 | 35 | h := New(logger, applier, gcr.Parse) 36 | 37 | msg := readFixture(t, "testdata/push_event.json") 38 | 39 | h.Handle(context.TODO(), msg) 40 | 41 | m.AssertPullRequestCreated(testGitHubRepo, &scm.PullRequestInput{ 42 | Body: fmt.Sprintf("Automated update from %q", testGcrRepo), 43 | Head: "test-branch-a", 44 | Base: "master", 45 | Title: "Automated image update", 46 | }) 47 | } 48 | 49 | func readFixture(t *testing.T, fixture string) *stubMessage { 50 | t.Helper() 51 | b, err := ioutil.ReadFile(fixture) 52 | if err != nil { 53 | t.Fatalf("failed to read %s: %s", fixture, err) 54 | } 55 | msg := &stubMessage{ 56 | data: b, 57 | } 58 | return msg 59 | } 60 | 61 | func createConfigs() *config.RepoConfiguration { 62 | return &config.RepoConfiguration{ 63 | Repositories: []*config.Repository{ 64 | { 65 | Name: testGcrRepo, 66 | SourceRepo: testGitHubRepo, 67 | SourceBranch: "master", 68 | FilePath: testFilePath, 69 | UpdateKey: "test.image", 70 | BranchGenerateName: "test-branch-", 71 | }, 72 | }, 73 | } 74 | } 75 | 76 | type stubMessage struct { 77 | data []byte 78 | } 79 | 80 | func (m *stubMessage) Ack() {} 81 | func (m *stubMessage) Data() []byte { return m.data } 82 | 83 | type stubNameGenerator struct { 84 | name string 85 | } 86 | 87 | func (s stubNameGenerator) PrefixedName(p string) string { 88 | return p + s.name 89 | } 90 | -------------------------------------------------------------------------------- /pkg/pubsubhandler/interface.go: -------------------------------------------------------------------------------- 1 | package pubsubhandler 2 | 3 | type message interface { 4 | Ack() 5 | Data() []byte 6 | } 7 | -------------------------------------------------------------------------------- /pkg/pubsubhandler/testdata/push_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "INSERT", 3 | "digest": "gcr.io/mynamespace/repository@sha256:6ec128e26cd5", 4 | "tag": "gcr.io/mynamespace/repository:latest" 5 | } -------------------------------------------------------------------------------- /tekton/configuring-custom-ca.md: -------------------------------------------------------------------------------- 1 | # Configuring a Custom CA Chain 2 | 3 | You may need image-updater to interact with Git servers exposed using TLS which do not have a certificate signed by a well-known Certificate Authority. 4 | 5 | In such cases you can use the `--insecure` flag, or even better, load your custom CA Chain into the container which runs the task. 6 | 7 | ## Get the CA Chain 8 | 9 | First, we need to get the CA Chain so we can configure the Tekton task to make use of it. Usually, the CA Chain can be obtained from the vendor/team who signed 10 | the certificate being used by your Git Server. You can get it using a web browser as well, the process of getting the CA Chain is out of the scope of this document. 11 | 12 | ## Configuring a ConfigMap with you CA Chain 13 | 14 | Once we have our CA Chain file with PEM format, we can go ahead and create a ConfigMap in order to store the CA Chain in the Kubernetes cluster where Tekton is running. 15 | 16 | The ConfigMap must be created in the same namespace where the Tekton task for `image-updater` is defined. 17 | 18 | ~~~sh 19 | kubectl -n create configmap custom-ca-chain --from-file=ca-bundle.crt= 20 | ~~~ 21 | 22 | ## Configuring a Volume on the Tekton Task 23 | 24 | We have our Custom CA Chain loaded into Kubernetes as a ConfigMap, now we need to configure the `image-updater` Tekton task to make use of it. 25 | 26 | You can find the definition of the `image-updater` Tekton Task [here](https://github.com/gitops-tools/image-updater/blob/main/tekton/image-updater.yaml). We are using this Task as reference. 27 | 28 | You need to add the `volumeMounts` and `volumes` sections to the Tekton Task spec. 29 | 30 | ~~~yaml 31 | 32 | steps: 33 | - name: update-image 34 | image: bigkevmcd/image-updater:latest 35 | args: 36 | - "update" 37 | - "--driver=$(params.driver)" 38 | - "--file-path=$(params.file-path)" 39 | - "--image-repo=$(params.image-repo)" 40 | - "--new-image-url=$(params.new-image-url)" 41 | - "--source-branch=$(params.source-branch)" 42 | - "--source-repo=$(params.source-repo)" 43 | - "--update-key=$(params.update-key)" 44 | - "--branch-generate-name=$(params.branch-generate-name)" 45 | - "--api-endpoint=$(params.api-endpoint)" 46 | - "--insecure=$(params.insecure)" 47 | env: 48 | - name: AUTH_TOKEN 49 | valueFrom: 50 | secretKeyRef: 51 | name: image-updater-secret 52 | key: token 53 | volumeMounts: 54 | - mountPath: /etc/pki/tls/certs/ 55 | name: custom-ca-chain 56 | volumes: 57 | - configMap: 58 | name: custom-ca-chain 59 | name: custom-ca-chain 60 | ~~~ 61 | 62 | At this point you will be able to connect to your Git server without the need of using `--insecure`. 63 | -------------------------------------------------------------------------------- /tekton/image-updater.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1beta1 3 | kind: Task 4 | metadata: 5 | name: image-updater 6 | spec: 7 | params: 8 | - name: driver 9 | type: string 10 | description: The driver to use for connecting, 'gitlab' or 'github'. 11 | - name: api-endpoint 12 | type: string 13 | description: > 14 | Required for private GitLab and Github installations 15 | e.g. https://gitlab.example.com leave blank otherwise. 16 | default: "" 17 | - name: file-path 18 | type: string 19 | description: Path within the source-repo to update 20 | - name: image-repo 21 | type: string 22 | description: Image repo e.g. org/repo that is being updated 23 | - name: new-image-url 24 | type: string 25 | description: Image URL to populate file with e.g. myorg/my-image:c2b4eff 26 | - name: source-branch 27 | type: string 28 | description: Branch to fetch for updating 29 | default: master 30 | - name: source-repo 31 | type: string 32 | description: Git repository to update e.g. org/repo 33 | - name: branch-generate-name 34 | type: string 35 | description: > 36 | Prefix for naming automatically generated branch, if empty, this will 37 | update source-branch 38 | default: "" 39 | - name: update-key 40 | type: string 41 | description: > 42 | JSON path within the file-path to update 43 | e.g. spec.template.spec.containers.0.image 44 | - name: insecure 45 | type: string 46 | description: Allow insecure server connections when using SSL 47 | default: "false" 48 | steps: 49 | - name: update-image 50 | image: bigkevmcd/image-updater:latest 51 | args: 52 | - "update" 53 | - "--driver=$(params.driver)" 54 | - "--file-path=$(params.file-path)" 55 | - "--image-repo=$(params.image-repo)" 56 | - "--new-image-url=$(params.new-image-url)" 57 | - "--source-branch=$(params.source-branch)" 58 | - "--source-repo=$(params.source-repo)" 59 | - "--update-key=$(params.update-key)" 60 | - "--branch-generate-name=$(params.branch-generate-name)" 61 | - "--api-endpoint=$(params.api-endpoint)" 62 | - "--insecure=$(params.insecure)" 63 | env: 64 | - name: AUTH_TOKEN 65 | valueFrom: 66 | secretKeyRef: 67 | name: image-updater-secret 68 | key: token 69 | -------------------------------------------------------------------------------- /test/errors.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | // MatchError checks errors against a regexp. 9 | // 10 | // Returns true if the string is empty and the error is nil. 11 | // Returns false if the string is not empty and the error is nil. 12 | // Otherwise returns the result of a regexp match against the string. 13 | func MatchError(t *testing.T, s string, e error) bool { 14 | t.Helper() 15 | if s == "" && e == nil { 16 | return true 17 | } 18 | if s != "" && e == nil { 19 | return false 20 | } 21 | match, err := regexp.MatchString(s, e.Error()) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | return match 26 | } 27 | --------------------------------------------------------------------------------