diff --git a/examples/gitops-kubernetes-application-pipeline/README.md b/examples/gitops-kubernetes-application-pipeline/README.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/gitops-kubernetes-application-pipeline/src/Dockerfile b/examples/gitops-kubernetes-application-pipeline/src/Dockerfile new file mode 100644 index 0000000..676cbef --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/Dockerfile @@ -0,0 +1,43 @@ +FROM golang:1.20-alpine as builder + +ARG REVISION + +RUN mkdir -p /podinfo/ + +WORKDIR /podinfo + +COPY . . + +RUN go mod download + +RUN CGO_ENABLED=0 go build -ldflags "-s -w \ + -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ + -a -o bin/podinfo cmd/podinfo/* + +RUN CGO_ENABLED=0 go build -ldflags "-s -w \ + -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ + -a -o bin/podcli cmd/podcli/* + +FROM alpine:3.17 + +ARG BUILD_DATE +ARG VERSION +ARG REVISION + +LABEL maintainer="stefanprodan" + +RUN addgroup -S app \ + && adduser -S -G app app \ + && apk --no-cache add \ + ca-certificates curl netcat-openbsd + +WORKDIR /home/app + +COPY --from=builder /podinfo/bin/podinfo . +COPY --from=builder /podinfo/bin/podcli /usr/local/bin/podcli +COPY ./ui ./ui +RUN chown -R app:app ./ + +USER app + +CMD ["./podinfo"] diff --git a/examples/gitops-kubernetes-application-pipeline/src/Dockerfile.base b/examples/gitops-kubernetes-application-pipeline/src/Dockerfile.base new file mode 100644 index 0000000..5d2f7c8 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/Dockerfile.base @@ -0,0 +1,10 @@ +FROM golang:1.20 + +WORKDIR /workspace + +# copy modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# cache modules +RUN go mod download diff --git a/examples/gitops-kubernetes-application-pipeline/src/Dockerfile.xx b/examples/gitops-kubernetes-application-pipeline/src/Dockerfile.xx new file mode 100644 index 0000000..7033148 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/Dockerfile.xx @@ -0,0 +1,53 @@ +ARG GO_VERSION=1.20 +ARG XX_VERSION=1.2.0 + +FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx + +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine as builder + +# Copy the build utilities. +COPY --from=xx / / + +ARG TARGETPLATFORM +ARG REVISION + +RUN mkdir -p /podinfo/ + +WORKDIR /podinfo + +COPY . . + +RUN go mod download + +ENV CGO_ENABLED=0 +RUN xx-go build -ldflags "-s -w \ + -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ + -a -o bin/podinfo cmd/podinfo/* + +RUN xx-go build -ldflags "-s -w \ + -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ + -a -o bin/podcli cmd/podcli/* + +FROM alpine:3.17 + +ARG BUILD_DATE +ARG VERSION +ARG REVISION + +LABEL maintainer="stefanprodan" + +RUN addgroup -S app \ + && adduser -S -G app app \ + && apk --no-cache add \ + ca-certificates curl netcat-openbsd + +WORKDIR /home/app + +COPY --from=builder /podinfo/bin/podinfo . +COPY --from=builder /podinfo/bin/podcli /usr/local/bin/podcli +COPY ./ui ./ui +RUN chown -R app:app ./ + +USER app + +CMD ["./podinfo"] diff --git a/examples/gitops-kubernetes-application-pipeline/src/LICENSE b/examples/gitops-kubernetes-application-pipeline/src/LICENSE new file mode 100644 index 0000000..1b92ec1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Stefan Prodan. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/gitops-kubernetes-application-pipeline/src/Makefile b/examples/gitops-kubernetes-application-pipeline/src/Makefile new file mode 100644 index 0000000..6717e6c --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/Makefile @@ -0,0 +1,105 @@ +# Makefile for releasing podinfo +# +# The release version is controlled from pkg/version + +TAG?=latest +NAME:=podinfo +DOCKER_REPOSITORY:=stefanprodan +DOCKER_IMAGE_NAME:=$(DOCKER_REPOSITORY)/$(NAME) +GIT_COMMIT:=$(shell git describe --dirty --always) +VERSION:=$(shell grep 'VERSION' pkg/version/version.go | awk '{ print $$4 }' | tr -d '"') +EXTRA_RUN_ARGS?= + +run: + go run -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$(GIT_COMMIT)" cmd/podinfo/* \ + --level=debug --grpc-port=9999 --backend-url=https://httpbin.org/status/401 --backend-url=https://httpbin.org/status/500 \ + --ui-logo=https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif $(EXTRA_RUN_ARGS) + +.PHONY: test +test: + go test ./... -coverprofile cover.out + +build: + GIT_COMMIT=$$(git rev-list -1 HEAD) && CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$(GIT_COMMIT)" -a -o ./bin/podinfo ./cmd/podinfo/* + GIT_COMMIT=$$(git rev-list -1 HEAD) && CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$(GIT_COMMIT)" -a -o ./bin/podcli ./cmd/podcli/* + +tidy: + rm -f go.sum; go mod tidy -compat=1.19 + +vet: + go vet ./... + +fmt: + gofmt -l -s -w ./ + goimports -l -w ./ + +build-charts: + helm lint charts/* + helm package charts/* + +build-container: + docker build -t $(DOCKER_IMAGE_NAME):$(VERSION) . + +build-xx: + docker buildx build \ + --platform=linux/amd64 \ + -t $(DOCKER_IMAGE_NAME):$(VERSION) \ + --load \ + -f Dockerfile.xx . + +build-base: + docker build -f Dockerfile.base -t $(DOCKER_REPOSITORY)/podinfo-base:latest . + +push-base: build-base + docker push $(DOCKER_REPOSITORY)/podinfo-base:latest + +test-container: + @docker rm -f podinfo || true + @docker run -dp 9898:9898 --name=podinfo $(DOCKER_IMAGE_NAME):$(VERSION) + @docker ps + @TOKEN=$$(curl -sd 'test' localhost:9898/token | jq -r .token) && \ + curl -sH "Authorization: Bearer $${TOKEN}" localhost:9898/token/validate | grep test + +push-container: + docker tag $(DOCKER_IMAGE_NAME):$(VERSION) $(DOCKER_IMAGE_NAME):latest + docker push $(DOCKER_IMAGE_NAME):$(VERSION) + docker push $(DOCKER_IMAGE_NAME):latest + docker tag $(DOCKER_IMAGE_NAME):$(VERSION) quay.io/$(DOCKER_IMAGE_NAME):$(VERSION) + docker tag $(DOCKER_IMAGE_NAME):$(VERSION) quay.io/$(DOCKER_IMAGE_NAME):latest + docker push quay.io/$(DOCKER_IMAGE_NAME):$(VERSION) + docker push quay.io/$(DOCKER_IMAGE_NAME):latest + +version-set: + @next="$(TAG)" && \ + current="$(VERSION)" && \ + /usr/bin/sed -i '' "s/$$current/$$next/g" pkg/version/version.go && \ + /usr/bin/sed -i '' "s/tag: $$current/tag: $$next/g" charts/podinfo/values.yaml && \ + /usr/bin/sed -i '' "s/tag: $$current/tag: $$next/g" charts/podinfo/values-prod.yaml && \ + /usr/bin/sed -i '' "s/appVersion: $$current/appVersion: $$next/g" charts/podinfo/Chart.yaml && \ + /usr/bin/sed -i '' "s/version: $$current/version: $$next/g" charts/podinfo/Chart.yaml && \ + /usr/bin/sed -i '' "s/podinfo:$$current/podinfo:$$next/g" kustomize/deployment.yaml && \ + /usr/bin/sed -i '' "s/podinfo:$$current/podinfo:$$next/g" deploy/webapp/frontend/deployment.yaml && \ + /usr/bin/sed -i '' "s/podinfo:$$current/podinfo:$$next/g" deploy/webapp/backend/deployment.yaml && \ + /usr/bin/sed -i '' "s/podinfo:$$current/podinfo:$$next/g" deploy/bases/frontend/deployment.yaml && \ + /usr/bin/sed -i '' "s/podinfo:$$current/podinfo:$$next/g" deploy/bases/backend/deployment.yaml && \ + /usr/bin/sed -i '' "s/$$current/$$next/g" cue/main.cue && \ + echo "Version $$next set in code, deployment, chart and kustomize" + +release: + git tag $(VERSION) + git push origin $(VERSION) + +swagger: + go install github.com/swaggo/swag/cmd/swag@latest + go get github.com/swaggo/swag/gen@latest + go get github.com/swaggo/swag/cmd/swag@latest + cd pkg/api && $$(go env GOPATH)/bin/swag init -g server.go + +.PHONY: cue-mod +cue-mod: + @cd cue && cue get go k8s.io/api/... + +.PHONY: cue-gen +cue-gen: + @cd cue && cue fmt ./... && cue vet --all-errors --concrete ./... + @cd cue && cue gen \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/README.md b/examples/gitops-kubernetes-application-pipeline/src/README.md new file mode 100644 index 0000000..0f26f61 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/README.md @@ -0,0 +1,197 @@ +# podinfo + +[![e2e](https://github.com/stefanprodan/podinfo/workflows/e2e/badge.svg)](https://github.com/stefanprodan/podinfo/blob/master/.github/workflows/e2e.yml) +[![test](https://github.com/stefanprodan/podinfo/workflows/test/badge.svg)](https://github.com/stefanprodan/podinfo/blob/master/.github/workflows/test.yml) +[![cve-scan](https://github.com/stefanprodan/podinfo/workflows/cve-scan/badge.svg)](https://github.com/stefanprodan/podinfo/blob/master/.github/workflows/cve-scan.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/stefanprodan/podinfo)](https://goreportcard.com/report/github.com/stefanprodan/podinfo) +[![Docker Pulls](https://img.shields.io/docker/pulls/stefanprodan/podinfo)](https://hub.docker.com/r/stefanprodan/podinfo) + +Podinfo is a tiny web application made with Go that showcases best practices of running microservices in Kubernetes. +Podinfo is used by CNCF projects like [Flux](https://github.com/fluxcd/flux2) and [Flagger](https://github.com/fluxcd/flagger) +for end-to-end testing and workshops. + +Specifications: + +* Health checks (readiness and liveness) +* Graceful shutdown on interrupt signals +* File watcher for secrets and configmaps +* Instrumented with Prometheus and Open Telemetry +* Structured logging with zap +* 12-factor app with viper +* Fault injection (random errors and latency) +* Swagger docs +* CUE, Helm and Kustomize installers +* End-to-End testing with Kubernetes Kind and Helm +* Multi-arch container image with Docker buildx and Github Actions +* Container image signing with Sigstore cosign +* SBOMs and SLSA Provenance embedded in the container image +* CVE scanning with Trivy + +Web API: + +* `GET /` prints runtime information +* `GET /version` prints podinfo version and git commit hash +* `GET /metrics` return HTTP requests duration and Go runtime metrics +* `GET /healthz` used by Kubernetes liveness probe +* `GET /readyz` used by Kubernetes readiness probe +* `POST /readyz/enable` signals the Kubernetes LB that this instance is ready to receive traffic +* `POST /readyz/disable` signals the Kubernetes LB to stop sending requests to this instance +* `GET /status/{code}` returns the status code +* `GET /panic` crashes the process with exit code 255 +* `POST /echo` forwards the call to the backend service and echos the posted content +* `GET /env` returns the environment variables as a JSON array +* `GET /headers` returns a JSON with the request HTTP headers +* `GET /delay/{seconds}` waits for the specified period +* `POST /token` issues a JWT token valid for one minute `JWT=$(curl -sd 'anon' podinfo:9898/token | jq -r .token)` +* `GET /token/validate` validates the JWT token `curl -H "Authorization: Bearer $JWT" podinfo:9898/token/validate` +* `GET /configs` returns a JSON with configmaps and/or secrets mounted in the `config` volume +* `POST/PUT /cache/{key}` saves the posted content to Redis +* `GET /cache/{key}` returns the content from Redis if the key exists +* `DELETE /cache/{key}` deletes the key from Redis if exists +* `POST /store` writes the posted content to disk at /data/hash and returns the SHA1 hash of the content +* `GET /store/{hash}` returns the content of the file /data/hash if exists +* `GET /ws/echo` echos content via websockets `podcli ws ws://localhost:9898/ws/echo` +* `GET /chunked/{seconds}` uses `transfer-encoding` type `chunked` to give a partial response and then waits for the specified period +* `GET /swagger.json` returns the API Swagger docs, used for Linkerd service profiling and Gloo routes discovery + +gRPC API: + +* `/grpc.health.v1.Health/Check` health checking + +Web UI: + +![podinfo-ui](https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/screens/podinfo-ui-v3.png) + +To access the Swagger UI open `/swagger/index.html` in a browser. + +### Guides + +* [GitOps Progressive Deliver with Flagger, Helm v3 and Linkerd](https://helm.workshop.flagger.dev/intro/) +* [GitOps Progressive Deliver on EKS with Flagger and AppMesh](https://eks.handson.flagger.dev/prerequisites/) +* [Automated canary deployments with Flagger and Istio](https://medium.com/google-cloud/automated-canary-deployments-with-flagger-and-istio-ac747827f9d1) +* [Kubernetes autoscaling with Istio metrics](https://medium.com/google-cloud/kubernetes-autoscaling-with-istio-metrics-76442253a45a) +* [Autoscaling EKS on Fargate with custom metrics](https://aws.amazon.com/blogs/containers/autoscaling-eks-on-fargate-with-custom-metrics/) +* [Managing Helm releases the GitOps way](https://medium.com/google-cloud/managing-helm-releases-the-gitops-way-207a6ac6ff0e) +* [Securing EKS Ingress With Contour And Let’s Encrypt The GitOps Way](https://aws.amazon.com/blogs/containers/securing-eks-ingress-contour-lets-encrypt-gitops/) + +### Install + +To install Podinfo on Kubernetes the minimum required version is **Kubernetes v1.23**. + +#### Helm + +Install from github.io: + +```bash +helm repo add podinfo https://stefanprodan.github.io/podinfo + +helm upgrade --install --wait frontend \ +--namespace test \ +--set replicaCount=2 \ +--set backend=http://backend-podinfo:9898/echo \ +podinfo/podinfo + +helm test frontend --namespace test + +helm upgrade --install --wait backend \ +--namespace test \ +--set redis.enabled=true \ +podinfo/podinfo +``` + +Install from ghcr.io: + +```bash +helm upgrade --install --wait podinfo --namespace default \ +oci://ghcr.io/stefanprodan/charts/podinfo +``` + +#### Kustomize + +```bash +kubectl apply -k github.com/stefanprodan/podinfo//kustomize +``` + +#### Docker + +```bash +docker run -dp 9898:9898 stefanprodan/podinfo +``` + +### Continuous Delivery + +In order to install podinfo on a Kubernetes cluster and keep it up to date with the latest +release in an automated manner, you can use [Flux](https://fluxcd.io). + +Install the Flux CLI on MacOS and Linux using Homebrew: + +```sh +brew install fluxcd/tap/flux +``` + +Install the Flux controllers needed for Helm operations: + +```sh +flux install \ +--namespace=flux-system \ +--network-policy=false \ +--components=source-controller,helm-controller +``` + +Add podinfo's Helm repository to your cluster and +configure Flux to check for new chart releases every ten minutes: + +```sh +flux create source helm podinfo \ +--namespace=default \ +--url=https://stefanprodan.github.io/podinfo \ +--interval=10m +``` + +Create a `podinfo-values.yaml` file locally: + +```sh +cat > podinfo-values.yaml <=1.23.0-0" diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/LICENSE b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/LICENSE new file mode 100644 index 0000000..1b92ec1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Stefan Prodan. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/README.md b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/README.md new file mode 100644 index 0000000..b81a5b9 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/README.md @@ -0,0 +1,130 @@ +# Podinfo + +Podinfo is a tiny web application made with Go +that showcases best practices of running microservices in Kubernetes. + +Podinfo is used by CNCF projects like [Flux](https://github.com/fluxcd/flux2) +and [Flagger](https://github.com/fluxcd/flagger) +for end-to-end testing and workshops. + +## Installing the Chart + +The Podinfo charts are published to +[GitHub Container Registry](https://github.com/stefanprodan/podinfo/pkgs/container/charts%2Fpodinfo) +and signed with [Cosign](https://github.com/sigstore/cosign) & GitHub Actions OIDC. + +To install the chart with the release name `my-release` from GHCR: + +```console +$ helm upgrade -i my-release oci://ghcr.io/stefanprodan/charts/podinfo +``` + +To verify a chart with Cosign: + +```console +$ cosign verify ghcr.io/stefanprodan/charts/podinfo: +``` + +Alternatively, you can install the chart from GitHub pages: + +```console +$ helm repo add podinfo https://stefanprodan.github.io/podinfo + +$ helm upgrade -i my-release podinfo/podinfo +``` + +The command deploys podinfo on the Kubernetes cluster in the default namespace. +The [configuration](#configuration) section lists the parameters that can be configured during installation. + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```console +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following tables lists the configurable parameters of the podinfo chart and their default values. + +| Parameter | Default | Description | +|-----------------------------------|------------------------|------------------------------------------------------------------------------------------------------------------------| +| `replicaCount` | `1` | Desired number of pods | +| `logLevel` | `info` | Log level: `debug`, `info`, `warn`, `error` | +| `backend` | `None` | Echo backend URL | +| `backends` | `[]` | Array of echo backend URLs | +| `cache` | `None` | Redis address in the format `tcp://:` | +| `redis.enabled` | `false` | Create Redis deployment for caching purposes | +| `ui.color` | `#34577c` | UI color | +| `ui.message` | `None` | UI greetings message | +| `ui.logo` | `None` | UI logo | +| `faults.delay` | `false` | Random HTTP response delays between 0 and 5 seconds | +| `faults.error` | `false` | 1/3 chances of a random HTTP response error | +| `faults.unhealthy` | `false` | When set, the healthy state is never reached | +| `faults.unready` | `false` | When set, the ready state is never reached | +| `faults.testFail` | `false` | When set, a helm test is included which always fails | +| `faults.testTimeout` | `false` | When set, a helm test is included which always times out | +| `image.repository` | `stefanprodan/podinfo` | Image repository | +| `image.tag` | `` | Image tag | +| `image.pullPolicy` | `IfNotPresent` | Image pull policy | +| `service.enabled` | `true` | Create a Kubernetes Service, should be disabled when using [Flagger](https://flagger.app) | +| `service.type` | `ClusterIP` | Type of the Kubernetes Service | +| `service.metricsPort` | `9797` | Prometheus metrics endpoint port | +| `service.httpPort` | `9898` | Container HTTP port | +| `service.externalPort` | `9898` | ClusterIP HTTP port | +| `service.grpcPort` | `9999` | ClusterIP gPRC port | +| `service.grpcService` | `podinfo` | gPRC service name | +| `service.nodePort` | `31198` | NodePort for the HTTP endpoint | +| `h2c.enabled` | `false` | Allow upgrading to h2c (non-TLS version of HTTP/2) | +| `hpa.enabled` | `false` | Enables the Kubernetes HPA | +| `hpa.maxReplicas` | `10` | Maximum amount of pods | +| `hpa.cpu` | `None` | Target CPU usage per pod | +| `hpa.memory` | `None` | Target memory usage per pod | +| `hpa.requests` | `None` | Target HTTP requests per second per pod | +| `serviceAccount.enabled` | `false` | Whether a service account should be created | +| `serviceAccount.name` | `None` | The name of the service account to use, if not set and create is true, a name is generated using the fullname template | +| `serviceAccount.imagePullSecrets` | `[]` | List of image pull secrets if pulling from private registries. | +| `securityContext` | `{}` | The security context to be set on the podinfo container | +| `linkerd.profile.enabled` | `false` | Create Linkerd service profile | +| `serviceMonitor.enabled` | `false` | Whether a Prometheus Operator service monitor should be created | +| `serviceMonitor.interval` | `15s` | Prometheus scraping interval | +| `serviceMonitor.additionalLabels` | `{}` | Add additional labels to the service monitor | +| `ingress.enabled` | `false` | Enables Ingress | +| `ingress.className ` | `""` | Use ingressClassName | +| `ingress.annotations` | `{}` | Ingress annotations | +| `ingress.hosts` | `[]` | Ingress accepted hosts | +| `ingress.tls` | `[]` | Ingress TLS configuration | +| `resources.requests.cpu` | `1m` | Pod CPU request | +| `resources.requests.memory` | `16Mi` | Pod memory request | +| `resources.limits.cpu` | `None` | Pod CPU limit | +| `resources.limits.memory` | `None` | Pod memory limit | +| `nodeSelector` | `{}` | Node labels for pod assignment | +| `tolerations` | `[]` | List of node taints to tolerate | +| `affinity` | `None` | Node/pod affinities | +| `podAnnotations` | `{}` | Pod annotations | + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```console +$ helm install my-release podinfo/podinfo \ + --set=serviceMonitor.enabled=true,serviceMonitor.interval=5s +``` + +To add custom annotations you need to escape the annotation key string: + +```console +$ helm upgrade -i my-release podinfo/podinfo \ +--set podAnnotations."appmesh\.k8s\.aws\/preview"=enabled +``` + +Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, + +```console +$ helm install my-release podinfo/podinfo -f values.yaml +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/NOTES.txt b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/NOTES.txt new file mode 100644 index 0000000..d832972 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/NOTES.txt @@ -0,0 +1,20 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "podinfo.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "podinfo.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "podinfo.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.externalPort }} +{{- else if contains "ClusterIP" .Values.service.type }} + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl -n {{ .Release.Namespace }} port-forward deploy/{{ template "podinfo.fullname" . }} 8080:{{ .Values.service.externalPort }} +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/_helpers.tpl b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/_helpers.tpl new file mode 100644 index 0000000..c691994 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/_helpers.tpl @@ -0,0 +1,69 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "podinfo.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "podinfo.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "podinfo.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "podinfo.labels" -}} +helm.sh/chart: {{ include "podinfo.chart" . }} +{{ include "podinfo.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "podinfo.selectorLabels" -}} +app.kubernetes.io/name: {{ include "podinfo.fullname" . }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "podinfo.serviceAccountName" -}} +{{- if .Values.serviceAccount.enabled }} +{{- default (include "podinfo.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create the name of the tls secret for secure port +*/}} +{{- define "podinfo.tlsSecretName" -}} +{{- $fullname := include "podinfo.fullname" . -}} +{{- default (printf "%s-tls" $fullname) .Values.tls.secretName }} +{{- end }} \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/certificate.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/certificate.yaml new file mode 100644 index 0000000..8b23809 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/certificate.yaml @@ -0,0 +1,16 @@ +{{- if .Values.certificate.create -}} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} +spec: + dnsNames: + {{- range .Values.certificate.dnsNames }} + - {{ . | quote }} + {{- end }} + secretName: {{ template "podinfo.tlsSecretName" . }} + issuerRef: + {{- .Values.certificate.issuerRef | toYaml | trimSuffix "\n" | nindent 4 }} +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/deployment.yaml new file mode 100644 index 0000000..1344609 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/deployment.yaml @@ -0,0 +1,189 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} +spec: + {{- if not .Values.hpa.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + selector: + matchLabels: + {{- include "podinfo.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "podinfo.selectorLabels" . | nindent 8 }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.service.httpPort }}" + {{- range $key, $value := .Values.podAnnotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + spec: + terminationGracePeriodSeconds: 30 + {{- if .Values.serviceAccount.enabled }} + serviceAccountName: {{ template "podinfo.serviceAccountName" . }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.securityContext }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- else if (or .Values.service.hostPort .Values.tls.hostPort) }} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- end }} + command: + - ./podinfo + - --port={{ .Values.service.httpPort | default 9898 }} + {{- if .Values.host }} + - --host={{ .Values.host }} + {{- end }} + {{- if .Values.tls.enabled }} + - --secure-port={{ .Values.tls.port }} + {{- end }} + {{- if .Values.tls.certPath }} + - --cert-path={{ .Values.tls.certPath }} + {{- end }} + {{- if .Values.service.metricsPort }} + - --port-metrics={{ .Values.service.metricsPort }} + {{- end }} + {{- if .Values.service.grpcPort }} + - --grpc-port={{ .Values.service.grpcPort }} + {{- end }} + {{- if .Values.service.grpcService }} + - --grpc-service-name={{ .Values.service.grpcService }} + {{- end }} + {{- range .Values.backends }} + - --backend-url={{ . }} + {{- end }} + {{- if .Values.cache }} + - --cache-server={{ .Values.cache }} + {{- else if .Values.redis.enabled }} + - --cache-server=tcp://{{ template "podinfo.fullname" . }}-redis:6379 + {{- end }} + - --level={{ .Values.logLevel }} + - --random-delay={{ .Values.faults.delay }} + - --random-error={{ .Values.faults.error }} + {{- if .Values.faults.unhealthy }} + - --unhealthy + {{- end }} + {{- if .Values.faults.unready }} + - --unready + {{- end }} + {{- if .Values.h2c.enabled }} + - --h2c + {{- end }} + env: + {{- if .Values.ui.message }} + - name: PODINFO_UI_MESSAGE + value: {{ quote .Values.ui.message }} + {{- end }} + {{- if .Values.ui.logo }} + - name: PODINFO_UI_LOGO + value: {{ .Values.ui.logo }} + {{- end }} + {{- if .Values.ui.color }} + - name: PODINFO_UI_COLOR + value: {{ quote .Values.ui.color }} + {{- end }} + {{- if .Values.backend }} + - name: PODINFO_BACKEND_URL + value: {{ .Values.backend }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.httpPort | default 9898 }} + protocol: TCP + {{- if .Values.service.hostPort }} + hostPort: {{ .Values.service.hostPort }} + {{- end }} + {{- if .Values.tls.enabled }} + - name: https + containerPort: {{ .Values.tls.port | default 9899 }} + protocol: TCP + {{- if .Values.tls.hostPort }} + hostPort: {{ .Values.tls.hostPort }} + {{- end }} + {{- end }} + {{- if .Values.service.metricsPort }} + - name: http-metrics + containerPort: {{ .Values.service.metricsPort }} + protocol: TCP + {{- end }} + {{- if .Values.service.grpcPort }} + - name: grpc + containerPort: {{ .Values.service.grpcPort }} + protocol: TCP + {{- end }} + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:{{ .Values.service.httpPort | default 9898 }}/healthz + {{- with .Values.probes.liveness }} + initialDelaySeconds: {{ .initialDelaySeconds | default 1 }} + timeoutSeconds: {{ .timeoutSeconds | default 5 }} + failureThreshold: {{ .failureThreshold | default 3 }} + successThreshold: {{ .successThreshold | default 1 }} + periodSeconds: {{ .periodSeconds | default 10 }} + {{- end }} + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:{{ .Values.service.httpPort | default 9898 }}/readyz + {{- with .Values.probes.readiness }} + initialDelaySeconds: {{ .initialDelaySeconds | default 1 }} + timeoutSeconds: {{ .timeoutSeconds | default 5 }} + failureThreshold: {{ .failureThreshold | default 3 }} + successThreshold: {{ .successThreshold | default 1 }} + periodSeconds: {{ .periodSeconds | default 10 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /data + {{- if .Values.tls.enabled }} + - name: tls + mountPath: {{ .Values.tls.certPath | default "/data/cert" }} + readOnly: true + {{- end }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + volumes: + - name: data + emptyDir: {} + {{- if .Values.tls.enabled }} + - name: tls + secret: + secretName: {{ template "podinfo.tlsSecretName" . }} + {{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/hpa.yaml new file mode 100644 index 0000000..6d768ae --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/hpa.yaml @@ -0,0 +1,41 @@ +{{- if .Values.hpa.enabled -}} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ template "podinfo.fullname" . }} + minReplicas: {{ .Values.replicaCount }} + maxReplicas: {{ .Values.hpa.maxReplicas }} + metrics: + {{- if .Values.hpa.cpu }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.hpa.cpu }} + {{- end }} + {{- if .Values.hpa.memory }} + - type: Resource + resource: + name: memory + target: + type: AverageValue + averageValue: {{ .Values.hpa.memory }} + {{- end }} + {{- if .Values.hpa.requests }} + - type: Pods + pods: + metric: + name: http_requests + target: + type: AverageValue + averageValue: {{ .Values.hpa.requests }} + {{- end }} +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/ingress.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/ingress.yaml new file mode 100644 index 0000000..93f9ae4 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "podinfo.fullname" . -}} +{{- $svcPort := .Values.service.externalPort -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/linkerd.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/linkerd.yaml new file mode 100644 index 0000000..a96e091 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/linkerd.yaml @@ -0,0 +1,98 @@ +{{- if .Values.linkerd.profile.enabled -}} +apiVersion: linkerd.io/v1alpha2 +kind: ServiceProfile +metadata: + name: {{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local + labels: + {{- include "podinfo.labels" . | nindent 4 }} +spec: + routes: + - condition: + method: GET + pathRegex: / + name: GET / + - condition: + method: POST + pathRegex: /api/echo + name: POST /api/echo + - condition: + method: GET + pathRegex: /api/info + name: GET /api/info + - condition: + method: GET + pathRegex: /chunked/[^/]* + name: GET /chunked/{seconds} + - condition: + method: GET + pathRegex: /delay/[^/]* + name: GET /delay/{seconds} + - condition: + method: GET + pathRegex: /env + name: GET /env + - condition: + method: GET + pathRegex: /headers + name: GET /headers + - condition: + method: GET + pathRegex: /healthz + name: GET /healthz + - condition: + method: GET + pathRegex: /metrics + name: GET /metrics + - condition: + method: GET + pathRegex: /panic + name: GET /panic + - condition: + method: GET + pathRegex: /readyz + name: GET /readyz + - condition: + method: POST + pathRegex: /readyz/disable + name: POST /readyz/disable + - condition: + method: POST + pathRegex: /readyz/enable + name: POST /readyz/enable + - condition: + method: GET + pathRegex: /status/[^/]* + name: GET /status/{code} + - condition: + method: POST + pathRegex: /cache + name: POST /cache + - condition: + method: GET + pathRegex: /cache/[^/]* + name: GET /cache/{hash} + - condition: + method: POST + pathRegex: /store + name: POST /store + - condition: + method: GET + pathRegex: /store/[^/]* + name: GET /store/{hash} + - condition: + method: POST + pathRegex: /token + name: POST /token + - condition: + method: POST + pathRegex: /token/validate + name: POST /token/validate + - condition: + method: GET + pathRegex: /version + name: GET /version + - condition: + method: POST + pathRegex: /ws/echo + name: POST /ws/echo +{{- end }} \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/config.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/config.yaml new file mode 100644 index 0000000..cd63785 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/config.yaml @@ -0,0 +1,12 @@ +{{- if .Values.redis.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "podinfo.fullname" . }}-redis +data: + redis.conf: | + maxmemory 64mb + maxmemory-policy allkeys-lru + save "" + appendonly no +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/deployment.yaml new file mode 100644 index 0000000..7888855 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/deployment.yaml @@ -0,0 +1,68 @@ +{{- if .Values.redis.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "podinfo.fullname" . }}-redis + labels: + app: {{ template "podinfo.fullname" . }}-redis +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: {{ template "podinfo.fullname" . }}-redis + template: + metadata: + labels: + app: {{ template "podinfo.fullname" . }}-redis + annotations: + checksum/config: {{ include (print $.Template.BasePath "/redis/config.yaml") . | sha256sum | quote }} + spec: + {{- if .Values.serviceAccount.enabled }} + serviceAccountName: {{ template "podinfo.serviceAccountName" . }} + {{- end }} + containers: + - name: redis + image: "{{ .Values.redis.repository }}:{{ .Values.redis.tag }}" + imagePullPolicy: IfNotPresent + command: + - redis-server + - "/redis-master/redis.conf" + ports: + - name: redis + containerPort: 6379 + protocol: TCP + livenessProbe: + tcpSocket: + port: redis + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi + volumeMounts: + - mountPath: /var/lib/redis + name: data + - mountPath: /redis-master + name: config + volumes: + - name: data + emptyDir: {} + - name: config + configMap: + name: {{ template "podinfo.fullname" . }}-redis + items: + - key: redis.conf + path: redis.conf +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/service.yaml new file mode 100644 index 0000000..e206851 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/redis/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.redis.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "podinfo.fullname" . }}-redis + labels: + app: {{ template "podinfo.fullname" . }}-redis +spec: + type: ClusterIP + selector: + app: {{ template "podinfo.fullname" . }}-redis + ports: + - name: redis + port: 6379 + protocol: TCP + targetPort: redis +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/service.yaml new file mode 100644 index 0000000..6014e78 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/service.yaml @@ -0,0 +1,36 @@ +{{- if .Values.service.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} +{{- with .Values.service.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: http + protocol: TCP + name: http + {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + {{- if .Values.tls.enabled }} + - port: {{ .Values.tls.port | default 9899 }} + targetPort: https + protocol: TCP + name: https + {{- end }} + {{- if .Values.service.grpcPort }} + - port: {{ .Values.service.grpcPort }} + targetPort: grpc + protocol: TCP + name: grpc + {{- end }} + selector: + {{- include "podinfo.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/serviceaccount.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/serviceaccount.yaml new file mode 100644 index 0000000..72ff524 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.enabled -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "podinfo.serviceAccountName" . }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} +{{- with .Values.serviceAccount.imagePullSecrets }} +imagePullSecrets: + {{- toYaml . | nindent 2 }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/servicemonitor.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/servicemonitor.yaml new file mode 100644 index 0000000..fa0c344 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/servicemonitor.yaml @@ -0,0 +1,22 @@ +{{- if .Values.serviceMonitor.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "podinfo.fullname" . }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + {{- with .Values.serviceMonitor.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - path: /metrics + port: http + interval: {{ .Values.serviceMonitor.interval }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "podinfo.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/cache.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/cache.yaml new file mode 100644 index 0000000..ed85651 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/cache.yaml @@ -0,0 +1,29 @@ +{{- if .Values.cache }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-cache-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: curl + image: curlimages/curl:7.69.0 + command: + - sh + - -c + - | + curl -sd 'data' ${PODINFO_SVC}/cache/test && + curl -s ${PODINFO_SVC}/cache/test | grep data && + curl -s -XDELETE ${PODINFO_SVC}/cache/test + env: + - name: PODINFO_SVC + value: "{{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.externalPort }}" + restartPolicy: Never +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/fail.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/fail.yaml new file mode 100644 index 0000000..a9f7f27 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/fail.yaml @@ -0,0 +1,21 @@ +{{- if .Values.faults.testFail }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-fault-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: fault + image: alpine:3.11 + command: ['/bin/sh'] + args: ['-c', 'exit 1'] + restartPolicy: Never +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/grpc.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/grpc.yaml new file mode 100644 index 0000000..20c3d2e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/grpc.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-grpc-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: grpc-health-probe + image: stefanprodan/grpc_health_probe:v0.3.0 + command: ['grpc_health_probe'] + args: ['-addr={{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.grpcPort }}'] + restartPolicy: Never diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/jwt.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/jwt.yaml new file mode 100644 index 0000000..a0bd4f8 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/jwt.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-jwt-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: tools + image: giantswarm/tiny-tools + command: + - sh + - -c + - | + TOKEN=$(curl -sd 'test' ${PODINFO_SVC}/token | jq -r .token) && + curl -sH "Authorization: Bearer ${TOKEN}" ${PODINFO_SVC}/token/validate | grep test + env: + - name: PODINFO_SVC + value: "{{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.externalPort }}" + restartPolicy: Never diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/service.yaml new file mode 100644 index 0000000..9551980 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/service.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-service-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: curl + image: curlimages/curl:7.69.0 + command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: "{{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.externalPort }}" + restartPolicy: Never diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/timeout.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/timeout.yaml new file mode 100644 index 0000000..fbbad41 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/timeout.yaml @@ -0,0 +1,21 @@ +{{- if .Values.faults.testTimeout }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-fault-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: fault + image: alpine:3.11 + command: ['/bin/sh'] + args: ['-c', 'while sleep 3600; do :; done'] + restartPolicy: Never +{{- end }} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/tls.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/tls.yaml new file mode 100644 index 0000000..5f3842a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/templates/tests/tls.yaml @@ -0,0 +1,27 @@ +{{- if .Values.tls.enabled -}} +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "podinfo.fullname" . }}-tls-test-{{ randAlphaNum 5 | lower }} + labels: + {{- include "podinfo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + sidecar.istio.io/inject: "false" + linkerd.io/inject: disabled + appmesh.k8s.aws/sidecarInjectorWebhook: disabled +spec: + containers: + - name: curl + image: curlimages/curl:7.69.0 + command: + - sh + - -c + - | + curl -sk ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: "https://{{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.tls.port }}" + restartPolicy: Never +{{- end }} \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/values-prod.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/values-prod.yaml new file mode 100644 index 0000000..fb30e65 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/values-prod.yaml @@ -0,0 +1,139 @@ +# Production values for podinfo. +# Includes Redis deployment and memory limits. + +replicaCount: 1 +logLevel: info +backend: #http://backend-podinfo:9898/echo +backends: [] + +image: + repository: ghcr.io/stefanprodan/podinfo + tag: 6.3.5 + pullPolicy: IfNotPresent + +ui: + color: "#34577c" + message: "" + logo: "" + +# failure conditions +faults: + delay: false + error: false + unhealthy: false + unready: false + testFail: false + testTimeout: false + +# Kubernetes Service settings +service: + enabled: true + annotations: {} + type: ClusterIP + metricsPort: 9797 + httpPort: 9898 + externalPort: 9898 + grpcPort: 9999 + grpcService: podinfo + nodePort: 31198 + +# enable h2c protocol (non-TLS version of HTTP/2) +h2c: + enabled: false + +# enable tls on the podinfo service +tls: + enabled: false + # the name of the secret used to mount the certificate key pair + secretName: + # the path where the certificate key pair will be mounted + certPath: /data/cert + # the port used to host the tls endpoint on the service + port: 9899 + # the port used to bind the tls port to the host + # NOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing + # in local clusters such as kind without port forwarding + hostPort: + +# create a certificate manager certificate (cert-manager required) +certificate: + create: false + # the issuer used to issue the certificate + issuerRef: + kind: ClusterIssuer + name: self-signed + # the hostname / subject alternative names for the certificate + dnsNames: + - podinfo + +# metrics-server add-on required +hpa: + enabled: true + maxReplicas: 5 + # average total CPU usage per pod (1-100) + cpu: 99 + # average memory usage per pod (100Mi-1Gi) + memory: + # average http requests per second per pod (k8s-prometheus-adapter) + requests: + +# Redis address in the format tcp://: +cache: "" +# Redis deployment +redis: + enabled: true + repository: redis + tag: 7.0.7 + +serviceAccount: + # Specifies whether a service account should be created + enabled: false + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + # List of image pull secrets if pulling from private registries + imagePullSecrets: [] + +# set container security context +securityContext: {} + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: podinfo.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +linkerd: + profile: + enabled: false + +# create Prometheus Operator monitor +serviceMonitor: + enabled: false + interval: 15s + additionalLabels: {} + +resources: + limits: + memory: 256Mi + requests: + cpu: 100m + memory: 64Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +podAnnotations: {} diff --git a/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/values.yaml b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/values.yaml new file mode 100644 index 0000000..f9fc11a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/charts/podinfo/values.yaml @@ -0,0 +1,157 @@ +# Default values for podinfo. + +replicaCount: 1 +logLevel: info +host: #0.0.0.0 +backend: #http://backend-podinfo:9898/echo +backends: [] + +image: + repository: ghcr.io/stefanprodan/podinfo + tag: 6.3.5 + pullPolicy: IfNotPresent + +ui: + color: "#34577c" + message: "" + logo: "" + +# failure conditions +faults: + delay: false + error: false + unhealthy: false + unready: false + testFail: false + testTimeout: false + +# Kubernetes Service settings +service: + enabled: true + annotations: {} + type: ClusterIP + metricsPort: 9797 + httpPort: 9898 + externalPort: 9898 + grpcPort: 9999 + grpcService: podinfo + nodePort: 31198 + # the port used to bind the http port to the host + # NOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing + # in local clusters such as kind without port forwarding + hostPort: + +# enable h2c protocol (non-TLS version of HTTP/2) +h2c: + enabled: false + +# enable tls on the podinfo service +tls: + enabled: false + # the name of the secret used to mount the certificate key pair + secretName: + # the path where the certificate key pair will be mounted + certPath: /data/cert + # the port used to host the tls endpoint on the service + port: 9899 + # the port used to bind the tls port to the host + # NOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing + # in local clusters such as kind without port forwarding + hostPort: + +# create a certificate manager certificate (cert-manager required) +certificate: + create: false + # the issuer used to issue the certificate + issuerRef: + kind: ClusterIssuer + name: self-signed + # the hostname / subject alternative names for the certificate + dnsNames: + - podinfo + +# metrics-server add-on required +hpa: + enabled: false + maxReplicas: 10 + # average total CPU usage per pod (1-100) + cpu: + # average memory usage per pod (100Mi-1Gi) + memory: + # average http requests per second per pod (k8s-prometheus-adapter) + requests: + +# Redis address in the format tcp://: +cache: "" +# Redis deployment +redis: + enabled: false + repository: redis + tag: 7.0.7 + +serviceAccount: + # Specifies whether a service account should be created + enabled: false + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + # List of image pull secrets if pulling from private registries + imagePullSecrets: [] + +# set container security context +securityContext: {} + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: podinfo.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +linkerd: + profile: + enabled: false + +# create Prometheus Operator monitor +serviceMonitor: + enabled: false + interval: 15s + additionalLabels: {} + +resources: + limits: + requests: + cpu: 1m + memory: 16Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +podAnnotations: {} + +# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes +probes: + readiness: + initialDelaySeconds: 1 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + periodSeconds: 10 + liveness: + initialDelaySeconds: 1 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + periodSeconds: 10 diff --git a/examples/gitops-kubernetes-application-pipeline/src/cloudbuild.yaml b/examples/gitops-kubernetes-application-pipeline/src/cloudbuild.yaml new file mode 100644 index 0000000..77ea928 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cloudbuild.yaml @@ -0,0 +1,4 @@ +steps: +- name: 'gcr.io/cloud-builders/docker' + args: ['build','-f' , 'Dockerfile', '-t', 'gcr.io/$PROJECT_ID/podinfo:$BRANCH_NAME-$SHORT_SHA', '.'] +images: ['gcr.io/$PROJECT_ID/podinfo:$BRANCH_NAME-$SHORT_SHA'] diff --git a/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/check.go b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/check.go new file mode 100644 index 0000000..e88958c --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/check.go @@ -0,0 +1,313 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" +) + +var ( + retryCount int + retryDelay time.Duration + method string + body string + timeout time.Duration + grpcServiceName string +) + +var checkCmd = &cobra.Command{ + Use: `check`, + Short: "Health check commands", + Long: "Commands for running health checks", +} + +var checkUrlCmd = &cobra.Command{ + Use: `http [address]`, + Short: "HTTP(S) health check", + Example: ` check http https://httpbin.org/anything --method=POST --retry=2 --delay=2s --timeout=3s --body='{"test"=1}'`, + RunE: runCheck, +} + +var checkTcpCmd = &cobra.Command{ + Use: `tcp [address]`, + Short: "TCP health check", + Example: ` check tcp httpbin.org:443 --retry=1 --delay=2s --timeout=2s`, + RunE: runCheckTCP, +} + +var checkCertCmd = &cobra.Command{ + Use: `cert [address]`, + Short: "SSL/TLS certificate validity check", + Example: ` check cert httpbin.org`, + RunE: runCheckCert, +} + +var checkgRPCCmd = &cobra.Command{ + Use: `grpc [address]`, + Short: "gRPC health check", + Example: ` check grpc localhost:8080 --service=podinfo --retry=1 --delay=2s --timeout=2s`, + RunE: runCheckgPRC, +} + +func init() { + checkUrlCmd.Flags().StringVar(&method, "method", "GET", "HTTP method") + checkUrlCmd.Flags().StringVar(&body, "body", "", "HTTP POST/PUT content") + checkUrlCmd.Flags().IntVar(&retryCount, "retry", 0, "times to retry the HTTP call") + checkUrlCmd.Flags().DurationVar(&retryDelay, "delay", 1*time.Second, "wait duration between retries") + checkUrlCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout") + checkCmd.AddCommand(checkUrlCmd) + + checkTcpCmd.Flags().IntVar(&retryCount, "retry", 0, "times to retry the TCP check") + checkTcpCmd.Flags().DurationVar(&retryDelay, "delay", 1*time.Second, "wait duration between retries") + checkTcpCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout") + checkCmd.AddCommand(checkTcpCmd) + + checkgRPCCmd.Flags().IntVar(&retryCount, "retry", 0, "times to retry the TCP check") + checkgRPCCmd.Flags().DurationVar(&retryDelay, "delay", 1*time.Second, "wait duration between retries") + checkgRPCCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout") + checkgRPCCmd.Flags().StringVar(&grpcServiceName, "service", "", "gRPC service name") + checkCmd.AddCommand(checkgRPCCmd) + + checkCmd.AddCommand(checkCertCmd) + + rootCmd.AddCommand(checkCmd) +} + +func runCheck(cmd *cobra.Command, args []string) error { + if retryCount < 0 { + return fmt.Errorf("--retry is required") + } + if len(args) < 1 { + return fmt.Errorf("address is required! example: check http https://httpbin.org") + } + + address := args[0] + if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { + address = fmt.Sprintf("http://%s", address) + } + + for n := 0; n <= retryCount; n++ { + if n != 1 { + time.Sleep(retryDelay) + } + + req, err := http.NewRequest(method, address, bytes.NewBuffer([]byte(body))) + if err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + os.Exit(1) + } + + ctx, cancel := context.WithTimeout(req.Context(), timeout) + resp, err := http.DefaultClient.Do(req.WithContext(ctx)) + cancel() + if err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + continue + } + + if resp.Body != nil { + resp.Body.Close() + } + + if resp.StatusCode >= 200 && resp.StatusCode < 400 { + logger.Info("check succeed", + zap.String("address", address), + zap.Int("status code", resp.StatusCode), + zap.String("response size", fmtContentLength(resp.ContentLength))) + os.Exit(0) + } else { + logger.Info("check failed", + zap.String("address", address), + zap.Int("status code", resp.StatusCode)) + continue + } + } + + os.Exit(1) + return nil +} + +func runCheckTCP(cmd *cobra.Command, args []string) error { + if retryCount < 0 { + return fmt.Errorf("--retry is required") + } + if len(args) < 1 { + return fmt.Errorf("address is required! example: check tcp httpbin.org:80") + } + address := args[0] + + for n := 0; n <= retryCount; n++ { + if n != 1 { + time.Sleep(retryDelay) + } + + conn, err := net.DialTimeout("tcp", address, timeout) + + if err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + continue + } + + conn.Close() + logger.Info("check succeed", zap.String("address", address)) + os.Exit(0) + + } + + os.Exit(1) + return nil +} + +func runCheckCert(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("address is required! example: check cert httpbin.org") + } + host := args[0] + if !strings.HasPrefix(host, "https://") { + host = "https://" + host + } + + u, err := url.Parse(host) + if err != nil { + logger.Info("check failed", + zap.String("address", host), + zap.Error(err)) + os.Exit(1) + } + + address := u.Hostname() + ":443" + ipConn, err := net.DialTimeout("tcp", address, 5*time.Second) + if err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + os.Exit(1) + + } + + defer ipConn.Close() + conn := tls.Client(ipConn, &tls.Config{ + InsecureSkipVerify: true, + ServerName: u.Hostname(), + }) + if err = conn.Handshake(); err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + os.Exit(1) + } + + defer conn.Close() + addr := conn.RemoteAddr() + _, _, err = net.SplitHostPort(addr.String()) + if err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + os.Exit(1) + } + + cert := conn.ConnectionState().PeerCertificates[0] + + timeNow := time.Now() + if timeNow.After(cert.NotAfter) { + logger.Info("check failed", + zap.String("address", address), + zap.String("issuer", cert.Issuer.CommonName), + zap.String("subject", cert.Subject.CommonName), + zap.Time("expired", cert.NotAfter)) + os.Exit(1) + } + + logger.Info("check succeed", + zap.String("address", address), + zap.String("issuer", cert.Issuer.CommonName), + zap.String("subject", cert.Subject.CommonName), + zap.Time("notAfter", cert.NotAfter), + zap.Time("notBefore", cert.NotBefore)) + + return nil +} + +func fmtContentLength(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) +} + +func runCheckgPRC(cmd *cobra.Command, args []string) error { + if retryCount < 0 { + return fmt.Errorf("--retry is required") + } + if len(args) < 1 { + return fmt.Errorf("address is required! example: check grpc localhost:8080") + } + address := args[0] + + for n := 0; n <= retryCount; n++ { + if n != 1 { + time.Sleep(retryDelay) + } + + conn, err := grpc.Dial(address, grpc.WithInsecure()) + if err != nil { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + continue + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + resp, err := grpc_health_v1.NewHealthClient(conn).Check(ctx, &grpc_health_v1.HealthCheckRequest{ + Service: grpcServiceName, + }) + cancel() + + if err != nil { + if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented { + logger.Info("gPRC health protocol not implemented") + os.Exit(1) + } else { + logger.Info("check failed", + zap.String("address", address), + zap.Error(err)) + } + continue + } + + conn.Close() + logger.Info("check succeed", + zap.String("status", resp.GetStatus().String())) + os.Exit(0) + + } + + os.Exit(1) + return nil +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/main.go b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/main.go new file mode 100644 index 0000000..f59c22b --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +var rootCmd = &cobra.Command{ + Use: "podcli", + Short: "podinfo command line", + Long: ` +podinfo command line utilities`, +} + +var ( + logger *zap.Logger +) + +func main() { + + var err error + logger, err = zap.NewDevelopment() + if err != nil { + log.Fatalf("can't initialize zap logger: %v", err) + } + defer logger.Sync() + + rootCmd.SetArgs(os.Args[1:]) + if err := rootCmd.Execute(); err != nil { + e := err.Error() + fmt.Println(strings.ToUpper(e[:1]) + e[1:]) + os.Exit(1) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/version.go b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/version.go new file mode 100644 index 0000000..cccb70d --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/version.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/stefanprodan/podinfo/pkg/version" +) + +func init() { + rootCmd.AddCommand(versionCmd) +} + +var versionCmd = &cobra.Command{ + Use: `version`, + Short: "Prints podcli version", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println(version.VERSION) + return nil + }, +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/ws.go b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/ws.go new file mode 100644 index 0000000..88339d8 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cmd/podcli/ws.go @@ -0,0 +1,143 @@ +package main + +import ( + "encoding/hex" + "fmt" + "net/http" + "net/url" + "os" + "regexp" + "strings" + + "github.com/chzyer/readline" + "github.com/fatih/color" + "github.com/gorilla/websocket" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +var origin string + +func init() { + wsCmd.Flags().StringVarP(&origin, "origin", "o", "", "websocket origin") + rootCmd.AddCommand(wsCmd) +} + +var wsCmd = &cobra.Command{ + Use: `ws [address]`, + Short: "Websocket client", + Example: ` ws localhost:9898/ws/echo`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("address is required") + } + + address := args[0] + if !strings.HasPrefix(address, "ws://") && !strings.HasPrefix(address, "wss://") { + address = fmt.Sprintf("ws://%s", address) + } + + dest, err := url.Parse(address) + if err != nil { + return err + } + if origin != "" { + } else { + originURL := *dest + if dest.Scheme == "wss" { + originURL.Scheme = "https" + } else { + originURL.Scheme = "http" + } + origin = originURL.String() + } + + err = connect(dest.String(), origin, &readline.Config{ + Prompt: "> ", + }) + if err != nil { + logger.Info("websocket closed", zap.Error(err)) + } + return nil + }, +} + +type session struct { + ws *websocket.Conn + rl *readline.Instance + errChan chan error +} + +func connect(url, origin string, rlConf *readline.Config) error { + headers := make(http.Header) + headers.Add("Origin", origin) + + ws, _, err := websocket.DefaultDialer.Dial(url, headers) + if err != nil { + return err + } + + rl, err := readline.NewEx(rlConf) + if err != nil { + return err + } + defer rl.Close() + + sess := &session{ + ws: ws, + rl: rl, + errChan: make(chan error), + } + + go sess.readConsole() + go sess.readWebsocket() + + return <-sess.errChan +} + +func (s *session) readConsole() { + for { + line, err := s.rl.Readline() + if err != nil { + s.errChan <- err + return + } + + err = s.ws.WriteMessage(websocket.TextMessage, []byte(line)) + if err != nil { + s.errChan <- err + return + } + } +} + +func bytesToFormattedHex(bytes []byte) string { + text := hex.EncodeToString(bytes) + return regexp.MustCompile("(..)").ReplaceAllString(text, "$1 ") +} + +func (s *session) readWebsocket() { + rxSprintf := color.New(color.FgGreen).SprintfFunc() + + for { + msgType, buf, err := s.ws.ReadMessage() + if err != nil { + fmt.Fprint(s.rl.Stdout(), rxSprintf("< %s\n", err.Error())) + os.Exit(1) + return + } + + var text string + switch msgType { + case websocket.TextMessage: + text = string(buf) + case websocket.BinaryMessage: + text = bytesToFormattedHex(buf) + default: + s.errChan <- fmt.Errorf("unknown websocket frame type: %d", msgType) + return + } + + fmt.Fprint(s.rl.Stdout(), rxSprintf("< %s\n", text)) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cmd/podinfo/main.go b/examples/gitops-kubernetes-application-pipeline/src/cmd/podinfo/main.go new file mode 100644 index 0000000..08a6be6 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cmd/podinfo/main.go @@ -0,0 +1,254 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/stefanprodan/podinfo/pkg/api" + "github.com/stefanprodan/podinfo/pkg/grpc" + "github.com/stefanprodan/podinfo/pkg/signals" + "github.com/stefanprodan/podinfo/pkg/version" + go_grpc "google.golang.org/grpc" +) + +func main() { + // flags definition + fs := pflag.NewFlagSet("default", pflag.ContinueOnError) + fs.String("host", "", "Host to bind service to") + fs.Int("port", 9898, "HTTP port to bind service to") + fs.Int("secure-port", 0, "HTTPS port") + fs.Int("port-metrics", 0, "metrics port") + fs.Int("grpc-port", 0, "gRPC port") + fs.String("grpc-service-name", "podinfo", "gPRC service name") + fs.String("level", "info", "log level debug, info, warn, error, fatal or panic") + fs.StringSlice("backend-url", []string{}, "backend service URL") + fs.Duration("http-client-timeout", 2*time.Minute, "client timeout duration") + fs.Duration("http-server-timeout", 30*time.Second, "server read and write timeout duration") + fs.Duration("server-shutdown-timeout", 5*time.Second, "server graceful shutdown timeout duration") + fs.String("data-path", "/data", "data local path") + fs.String("config-path", "", "config dir path") + fs.String("cert-path", "/data/cert", "certificate path for HTTPS port") + fs.String("config", "config.yaml", "config file name") + fs.String("ui-path", "./ui", "UI local path") + fs.String("ui-logo", "", "UI logo") + fs.String("ui-color", "#34577c", "UI color") + fs.String("ui-message", fmt.Sprintf("greetings from podinfo v%v", version.VERSION), "UI message") + fs.Bool("h2c", false, "allow upgrading to H2C") + fs.Bool("random-delay", false, "between 0 and 5 seconds random delay by default") + fs.String("random-delay-unit", "s", "either s(seconds) or ms(milliseconds") + fs.Int("random-delay-min", 0, "min for random delay: 0 by default") + fs.Int("random-delay-max", 5, "max for random delay: 5 by default") + fs.Bool("random-error", false, "1/3 chances of a random response error") + fs.Bool("unhealthy", false, "when set, healthy state is never reached") + fs.Bool("unready", false, "when set, ready state is never reached") + fs.Int("stress-cpu", 0, "number of CPU cores with 100 load") + fs.Int("stress-memory", 0, "MB of data to load into memory") + fs.String("cache-server", "", "Redis address in the format 'tcp://:'") + fs.String("otel-service-name", "", "service name for reporting to open telemetry address, when not set tracing is disabled") + + versionFlag := fs.BoolP("version", "v", false, "get version number") + + // parse flags + err := fs.Parse(os.Args[1:]) + switch { + case err == pflag.ErrHelp: + os.Exit(0) + case err != nil: + fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error()) + fs.PrintDefaults() + os.Exit(2) + case *versionFlag: + fmt.Println(version.VERSION) + os.Exit(0) + } + + // bind flags and environment variables + viper.BindPFlags(fs) + viper.RegisterAlias("backendUrl", "backend-url") + hostname, _ := os.Hostname() + viper.SetDefault("jwt-secret", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") + viper.SetDefault("ui-logo", "https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif") + viper.Set("hostname", hostname) + viper.Set("version", version.VERSION) + viper.Set("revision", version.REVISION) + viper.SetEnvPrefix("PODINFO") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + + // load config from file + if _, fileErr := os.Stat(filepath.Join(viper.GetString("config-path"), viper.GetString("config"))); fileErr == nil { + viper.SetConfigName(strings.Split(viper.GetString("config"), ".")[0]) + viper.AddConfigPath(viper.GetString("config-path")) + if readErr := viper.ReadInConfig(); readErr != nil { + fmt.Printf("Error reading config file, %v\n", readErr) + } + } + + // configure logging + logger, _ := initZap(viper.GetString("level")) + defer logger.Sync() + stdLog := zap.RedirectStdLog(logger) + defer stdLog() + + // start stress tests if any + beginStressTest(viper.GetInt("stress-cpu"), viper.GetInt("stress-memory"), logger) + + // validate port + if _, err := strconv.Atoi(viper.GetString("port")); err != nil { + port, _ := fs.GetInt("port") + viper.Set("port", strconv.Itoa(port)) + } + + // validate secure port + if _, err := strconv.Atoi(viper.GetString("secure-port")); err != nil { + securePort, _ := fs.GetInt("secure-port") + viper.Set("secure-port", strconv.Itoa(securePort)) + } + + // validate random delay options + if viper.GetInt("random-delay-max") < viper.GetInt("random-delay-min") { + logger.Panic("`--random-delay-max` should be greater than `--random-delay-min`") + } + + switch delayUnit := viper.GetString("random-delay-unit"); delayUnit { + case + "s", + "ms": + break + default: + logger.Panic("`random-delay-unit` accepted values are: s|ms") + } + + // load gRPC server config + var grpcCfg grpc.Config + if err := viper.Unmarshal(&grpcCfg); err != nil { + logger.Panic("config unmarshal failed", zap.Error(err)) + } + + // start gRPC server + var grpcServer *go_grpc.Server + if grpcCfg.Port > 0 { + grpcSrv, _ := grpc.NewServer(&grpcCfg, logger) + grpcServer = grpcSrv.ListenAndServe() + } + + // load HTTP server config + var srvCfg api.Config + if err := viper.Unmarshal(&srvCfg); err != nil { + logger.Panic("config unmarshal failed", zap.Error(err)) + } + + // log version and port + logger.Info("Starting podinfo", + zap.String("version", viper.GetString("version")), + zap.String("revision", viper.GetString("revision")), + zap.String("port", srvCfg.Port), + ) + + // start HTTP server + srv, _ := api.NewServer(&srvCfg, logger) + httpServer, httpsServer, healthy, ready := srv.ListenAndServe() + + // graceful shutdown + stopCh := signals.SetupSignalHandler() + sd, _ := signals.NewShutdown(srvCfg.ServerShutdownTimeout, logger) + sd.Graceful(stopCh, httpServer, httpsServer, grpcServer, healthy, ready) +} + +func initZap(logLevel string) (*zap.Logger, error) { + level := zap.NewAtomicLevelAt(zapcore.InfoLevel) + switch logLevel { + case "debug": + level = zap.NewAtomicLevelAt(zapcore.DebugLevel) + case "info": + level = zap.NewAtomicLevelAt(zapcore.InfoLevel) + case "warn": + level = zap.NewAtomicLevelAt(zapcore.WarnLevel) + case "error": + level = zap.NewAtomicLevelAt(zapcore.ErrorLevel) + case "fatal": + level = zap.NewAtomicLevelAt(zapcore.FatalLevel) + case "panic": + level = zap.NewAtomicLevelAt(zapcore.PanicLevel) + } + + zapEncoderConfig := zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } + + zapConfig := zap.Config{ + Level: level, + Development: false, + Sampling: &zap.SamplingConfig{ + Initial: 100, + Thereafter: 100, + }, + Encoding: "json", + EncoderConfig: zapEncoderConfig, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + + return zapConfig.Build() +} + +var stressMemoryPayload []byte + +func beginStressTest(cpus int, mem int, logger *zap.Logger) { + done := make(chan int) + if cpus > 0 { + logger.Info("starting CPU stress", zap.Int("cores", cpus)) + for i := 0; i < cpus; i++ { + go func() { + for { + select { + case <-done: + return + default: + + } + } + }() + } + } + + if mem > 0 { + path := "/tmp/podinfo.data" + f, err := os.Create(path) + + if err != nil { + logger.Error("memory stress failed", zap.Error(err)) + } + + if err := f.Truncate(1000000 * int64(mem)); err != nil { + logger.Error("memory stress failed", zap.Error(err)) + } + + stressMemoryPayload, err = os.ReadFile(path) + f.Close() + os.Remove(path) + if err != nil { + logger.Error("memory stress failed", zap.Error(err)) + } + logger.Info("starting MEMORY stress", zap.Int("memory", len(stressMemoryPayload))) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/README.md b/examples/gitops-kubernetes-application-pipeline/src/cue/README.md new file mode 100644 index 0000000..33db090 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/README.md @@ -0,0 +1,58 @@ +# Podinfo CUE module + +This directory contains a [CUE](https://cuelang.org/docs/) module and tooling +for generating podinfo's Kubernetes resources. + +The module contains a `podinfo.#Application` definition which takes `podinfo.#Config` as input. + +## Prerequisites + +Install CUE with: + +```shell +brew install cue +``` + +Generate the Kubernetes API definitions required by this module with: + +```shell +cue get go k8s.io/api/... +``` + +## Configuration + +Configure the application in `main.cue`: + +```cue +app: podinfo.#Application & { + config: { + meta: { + name: "podinfo" + namespace: "default" + } + image: tag: "6.1.3" + resources: requests: { + cpu: "100m" + memory: "16Mi" + } + hpa: { + enabled: true + maxReplicas: 3 + } + ingress: { + enabled: true + className: "nginx" + host: "podinfo.example.com" + tls: true + annotations: "cert-manager.io/cluster-issuer": "letsencrypt" + } + serviceMonitor: enabled: true + } +} +``` + +## Generate the manifests + +```shell +cue gen +``` diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/cue.mod/module.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/cue.mod/module.cue new file mode 100644 index 0000000..05f9e86 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/cue.mod/module.cue @@ -0,0 +1 @@ +module: "github.com/stefanprodan/podinfo/cue" diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/go.mod b/examples/gitops-kubernetes-application-pipeline/src/cue/go.mod new file mode 100644 index 0000000..d28088e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/go.mod @@ -0,0 +1,23 @@ +module github.com/stefanprodan/podinfo/cue + +go 1.17 + +require ( + github.com/go-logr/logr v1.2.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/api v0.23.5 // indirect + k8s.io/apimachinery v0.23.5 // indirect + k8s.io/klog/v2 v2.30.0 // indirect + k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect + sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect +) diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/go.sum b/examples/gitops-kubernetes-application-pipeline/src/cue/go.sum new file mode 100644 index 0000000..4779517 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/go.sum @@ -0,0 +1,231 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/main.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/main.cue new file mode 100644 index 0000000..1fc26f0 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/main.cue @@ -0,0 +1,33 @@ +package main + +import ( + podinfo "github.com/stefanprodan/podinfo/cue/podinfo" +) + +app: podinfo.#Application & { + config: { + meta: { + name: "podinfo" + namespace: "default" + } + image: tag: "6.3.5" + resources: requests: { + cpu: "100m" + memory: "16Mi" + } + hpa: { + enabled: true + maxReplicas: 3 + } + ingress: { + enabled: true + className: "nginx" + host: "podinfo.example.com" + tls: true + annotations: "cert-manager.io/cluster-issuer": "letsencrypt" + } + serviceMonitor: enabled: true + } +} + +objects: app.objects diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/main_tool.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/main_tool.cue new file mode 100644 index 0000000..38dcba1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/main_tool.cue @@ -0,0 +1,12 @@ +package main + +import ( + "tool/cli" + "encoding/yaml" +) + +command: gen: { + task: print: cli.Print & { + text: yaml.MarshalStream([ for x in objects {x}]) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/app.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/app.cue new file mode 100644 index 0000000..9ba271a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/app.cue @@ -0,0 +1,26 @@ +package podinfo + +#Application: { + config: #Config + + objects: { + service: #Service & {_config: config} + account: #ServiceAccount & {_config: config} + deployment: #Deployment & { + _config: config + _serviceAccount: account.metadata.name + } + } + + if config.hpa.enabled == true { + objects: hpa: #HorizontalPodAutoscaler & {_config: config} + } + + if config.ingress.enabled == true { + objects: ingress: #Ingress & {_config: config} + } + + if config.serviceMonitor.enabled == true { + objects: serviceMonitor: #ServiceMonitor & {_config: config} + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/config.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/config.cue new file mode 100644 index 0000000..d536dfc --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/config.cue @@ -0,0 +1,41 @@ +package podinfo + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" +) + +#Config: { + meta: metav1.#ObjectMeta + hpa: #hpaConfig + ingress: #ingressConfig + service: #serviceConfig + serviceMonitor: #serviceMonConfig + + image: { + repository: *"ghcr.io/stefanprodan/podinfo" | string + pullPolicy: *"IfNotPresent" | string + tag: string + } + + cache?: string & =~"^tcp://" + backends: [...string] + logLevel: *"info" | string + replicas: *1 | int + + resources: *{ + requests: { + cpu: "1m" + memory: "16Mi" + } + limits: memory: "128Mi" + } | corev1.#ResourceRequirements + + selectorLabels: *{"app.kubernetes.io/name": meta.name} | {[ string]: string} + meta: annotations: *{"app.kubernetes.io/version": "\(image.tag)"} | {[ string]: string} + meta: labels: *selectorLabels | {[ string]: string} + + securityContext?: corev1.#PodSecurityContext + affinity?: corev1.#Affinity + tolerations?: [ ...corev1.#Toleration] +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/deployment.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/deployment.cue new file mode 100644 index 0000000..2ebeec2 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/deployment.cue @@ -0,0 +1,110 @@ +package podinfo + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +#Deployment: appsv1.#Deployment & { + _config: #Config + _serviceAccount: string + apiVersion: "apps/v1" + kind: "Deployment" + metadata: _config.meta + spec: appsv1.#DeploymentSpec & { + if !_config.hpa.enabled { + replicas: _config.replicas + } + strategy: { + type: "RollingUpdate" + rollingUpdate: maxUnavailable: 1 + } + selector: matchLabels: _config.selectorLabels + template: { + metadata: { + labels: _config.selectorLabels + if !_config.serviceMonitor.enabled { + annotations: { + "prometheus.io/scrape": "true" + "prometheus.io/port": "\(_config.service.metricsPort)" + } + } + } + spec: corev1.#PodSpec & { + terminationGracePeriodSeconds: 15 + serviceAccountName: _serviceAccount + containers: [ + { + name: "podinfo" + image: "\(_config.image.repository):\(_config.image.tag)" + imagePullPolicy: _config.image.pullPolicy + command: [ + "./podinfo", + "--port=\(_config.service.httpPort)", + "--port-metrics=\(_config.service.metricsPort)", + "--grpc-port=\(_config.service.grpcPort)", + "--level=\(_config.logLevel)", + if _config.cache != _|_ { + "--cache-server=\(_config.cache)" + }, + for b in _config.backends { + "--backend-url=\(b)" + }, + ] + ports: [ + { + name: "http" + containerPort: _config.service.httpPort + protocol: "TCP" + }, + { + name: "http-metrics" + containerPort: _config.service.metricsPort + protocol: "TCP" + }, + { + name: "grpc" + containerPort: _config.service.grpcPort + protocol: "TCP" + }, + ] + livenessProbe: { + httpGet: { + path: "/healthz" + port: "http" + } + } + readinessProbe: { + httpGet: { + path: "/readyz" + port: "http" + } + } + volumeMounts: [ + { + name: "data" + mountPath: "/data" + }, + ] + resources: _config.resources + if _config.securityContext != _|_ { + securityContext: _config.securityContext + } + }, + ] + if _config.affinity != _|_ { + affinity: _config.affinity + } + if _config.tolerations != _|_ { + tolerations: _config.tolerations + } + volumes: [ + { + name: "data" + emptyDir: {} + }, + ] + } + } + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/hpa.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/hpa.cue new file mode 100644 index 0000000..2e755e0 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/hpa.cue @@ -0,0 +1,55 @@ +package podinfo + +import ( + autoscaling "k8s.io/api/autoscaling/v2" +) + +#hpaConfig: { + enabled: *false | bool + cpu: *99 | int + memory: *"" | string + minReplicas: *1 | int + maxReplicas: *1 | int +} + +#HorizontalPodAutoscaler: autoscaling.#HorizontalPodAutoscaler & { + _config: #Config + apiVersion: "autoscaling/v2" + kind: "HorizontalPodAutoscaler" + metadata: _config.meta + spec: { + scaleTargetRef: { + apiVersion: "apps/v1" + kind: "Deployment" + name: _config.meta.name + } + minReplicas: _config.hpa.minReplicas + maxReplicas: _config.hpa.maxReplicas + metrics: [ + if _config.hpa.cpu > 0 { + { + type: "Resource" + resource: { + name: "cpu" + target: { + type: "Utilization" + averageUtilization: _config.hpa.cpu + } + } + } + }, + if _config.hpa.memory != "" { + { + type: "Resource" + resource: { + name: "memory" + target: { + type: "AverageValue" + averageValue: _config.hpa.memory + } + } + } + }, + ] + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/ingress.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/ingress.cue new file mode 100644 index 0000000..d26d0af --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/ingress.cue @@ -0,0 +1,47 @@ +package podinfo + +import ( + netv1 "k8s.io/api/networking/v1" +) + +#ingressConfig: { + enabled: *false | bool + annotations?: {[ string]: string} + className?: string + tls: *false | bool + host: string +} + +#Ingress: netv1.#Ingress & { + _config: #Config + apiVersion: "networking.k8s.io/v1" + kind: "Ingress" + metadata: _config.meta + if _config.ingress.annotations != _|_ { + metadata: annotations: _config.ingress.annotations + } + spec: netv1.#IngressSpec & { + rules: [{ + host: _config.ingress.host + http: { + paths: [{ + pathType: "Prefix" + path: "/" + backend: service: { + name: _config.meta.name + port: name: "http" + } + }] + } + }] + if _config.ingress.tls { + tls: [{ + hosts: [_config.ingress.host] + secretName: "\(_config.meta.name)-cert" + }] + } + if _config.ingress.className != _|_ { + ingressClassName: _config.ingress.className + } + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/service.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/service.cue new file mode 100644 index 0000000..15e2cab --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/service.cue @@ -0,0 +1,44 @@ +package podinfo + +import ( + corev1 "k8s.io/api/core/v1" +) + +#serviceConfig: { + type: *"ClusterIP" | string + externalPort: *9898 | int + httpPort: *9898 | int + metricsPort: *9797 | int + grpcPort: *9999 | int +} + +#Service: corev1.#Service & { + _config: #Config + apiVersion: "v1" + kind: "Service" + metadata: _config.meta + spec: corev1.#ServiceSpec & { + type: _config.service.type + selector: _config.selectorLabels + ports: [ + { + name: "http" + port: _config.service.externalPort + targetPort: "\(name)" + protocol: "TCP" + }, + { + name: "http-metrics" + port: _config.service.metricsPort + targetPort: "\(name)" + protocol: "TCP" + }, + { + name: "grpc" + port: _config.service.grpcPort + targetPort: "\(name)" + protocol: "TCP" + }, + ] + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/serviceaccount.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/serviceaccount.cue new file mode 100644 index 0000000..90bf9f4 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/serviceaccount.cue @@ -0,0 +1,12 @@ +package podinfo + +import ( + corev1 "k8s.io/api/core/v1" +) + +#ServiceAccount: corev1.#ServiceAccount & { + _config: #Config + apiVersion: "v1" + kind: "ServiceAccount" + metadata: _config.meta +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/servicemonitor.cue b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/servicemonitor.cue new file mode 100644 index 0000000..8d232c3 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/cue/podinfo/servicemonitor.cue @@ -0,0 +1,22 @@ +package podinfo + +#serviceMonConfig: { + enabled: *false | bool + interval: *"15s" | string +} + +#ServiceMonitor: { + _config: #Config + apiVersion: "monitoring.coreos.com/v1" + kind: "ServiceMonitor" + metadata: _config.meta + spec: { + endpoints: [{ + path: "/metrics" + port: "http-metrics" + interval: _config.serviceMonitor.interval + }] + namespaceSelector: matchNames: [_config.meta.namespace] + selector: matchLabels: _config.meta.labels + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/README.md b/examples/gitops-kubernetes-application-pipeline/src/deploy/README.md new file mode 100644 index 0000000..4aef4bf --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/README.md @@ -0,0 +1,45 @@ +# Deploy demo webapp + +Demo webapp manifests: + +- [common](webapp/common) +- [frontend](webapp/frontend) +- [backend](webapp/backend) + +Deploy the demo in `webapp` namespace: + +```bash +kubectl apply -f ./webapp/common +kubectl apply -f ./webapp/backend +kubectl apply -f ./webapp/frontend +``` + +Deploy the demo in the `dev` namespace: + +```bash +kustomize build ./overlays/dev | kubectl apply -f- +``` + +Deploy the demo in the `staging` namespace: + +```bash +kustomize build ./overlays/staging | kubectl apply -f- +``` + +Deploy the demo in the `production` namespace: + +```bash +kustomize build ./overlays/production | kubectl apply -f- +``` + +## Testing Locally Using Kind + +> NOTE: You can install [kind from here](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) + +The following will create a new cluster called "podinfo" and configure host ports on 80 and 443. You can access the +endpoints on localhost. The example also deploys cert-manager within the cluster along with a self-signed cluster issuer +used to generate the certificate to validate the secure port. + +```sh +./kind.sh +``` diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/deployment.yaml new file mode 100644 index 0000000..f3bf40a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: backend + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: backend + spec: + containers: + - name: backend + image: ghcr.io/stefanprodan/podinfo:6.3.5 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=backend + - --level=info + - --cache-server=cache:6379 + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 32Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/hpa.yaml new file mode 100644 index 0000000..83a8c9a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: backend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: backend + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/kustomization.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/kustomization.yaml new file mode 100644 index 0000000..f753976 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - service.yaml + - deployment.yaml + - hpa.yaml + diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/service.yaml new file mode 100644 index 0000000..14857c4 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/backend/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend +spec: + type: ClusterIP + selector: + app: backend + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/deployment.yaml new file mode 100644 index 0000000..3e7e5c0 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cache +spec: + selector: + matchLabels: + app: cache + template: + metadata: + labels: + app: cache + spec: + containers: + - name: redis + image: redis:7.0.7 + imagePullPolicy: IfNotPresent + command: + - redis-server + - "/redis-master/redis.conf" + ports: + - name: redis + containerPort: 6379 + protocol: TCP + livenessProbe: + tcpSocket: + port: redis + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi + volumeMounts: + - mountPath: /var/lib/redis + name: data + - mountPath: /redis-master + name: config + volumes: + - name: data + emptyDir: {} + - name: config + configMap: + name: redis-config + items: + - key: redis.conf + path: redis.conf \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/kustomization.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/kustomization.yaml new file mode 100644 index 0000000..27169de --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - service.yaml + - deployment.yaml +configMapGenerator: + - name: redis-config + files: + - redis.conf diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/redis.conf b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/redis.conf new file mode 100644 index 0000000..4de7850 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/redis.conf @@ -0,0 +1,4 @@ +maxmemory 64mb +maxmemory-policy allkeys-lru +save "" +appendonly no diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/service.yaml new file mode 100644 index 0000000..6f9f67d --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/cache/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: cache +spec: + type: ClusterIP + selector: + app: cache + ports: + - name: redis + port: 6379 + protocol: TCP + targetPort: redis diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/deployment.yaml new file mode 100644 index 0000000..58d3df6 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend + spec: + containers: + - name: frontend + image: ghcr.io/stefanprodan/podinfo:6.3.5 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --level=info + - --backend-url=http://backend:9898/echo + - --cache-server=cache:6379 + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/hpa.yaml new file mode 100644 index 0000000..03223fc --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: 1 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/kustomization.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/kustomization.yaml new file mode 100644 index 0000000..f753976 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - service.yaml + - deployment.yaml + - hpa.yaml + diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/service.yaml new file mode 100644 index 0000000..8bf71a7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/bases/frontend/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend +spec: + type: ClusterIP + selector: + app: frontend + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/kind.sh b/examples/gitops-kubernetes-application-pipeline/src/deploy/kind.sh new file mode 100755 index 0000000..f819944 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/kind.sh @@ -0,0 +1,48 @@ +#! /usr/bin/env sh + +mkdir -p bin +cat > ./bin/kind.yaml <- + The Buildpacks task builds source into a container image and pushes it to a registry, + using Cloud Native Buildpacks. + + workspaces: + - name: source + description: Directory where application source is located. + + params: + - name: APP_IMAGE + description: The name of where to store the app image. + - name: BUILDER_IMAGE + description: The image on which builds will run (must include lifecycle and compatible buildpacks). + - name: SOURCE_SUBPATH + description: A subpath within the `source` input where the source to build is located. + default: "" + - name: ENV_VARS + type: array + description: Environment variables to set during _build-time_. + default: [] + - name: PROCESS_TYPE + description: The default process type to set on the image. + default: "web" + - name: RUN_IMAGE + description: Reference to a run image to use. + default: "" + - name: CACHE_IMAGE + description: The name of the persistent app cache image (if no cache workspace is provided). + default: "" + - name: SKIP_RESTORE + description: Do not write layer metadata or restore cached layers. + default: "false" + - name: USER_ID + description: The user ID of the builder image user. + default: "1000" + - name: GROUP_ID + description: The group ID of the builder image user. + default: "1000" + - name: PLATFORM_DIR + description: The name of the platform directory. + default: empty-dir + + results: + - name: APP_IMAGE_DIGEST + description: The digest of the built `APP_IMAGE`. + + stepTemplate: + env: + - name: CNB_PLATFORM_API + value: "0.4" + + steps: + - name: prepare + image: docker.io/library/bash:5.1.4@sha256:b208215a4655538be652b2769d82e576bc4d0a2bb132144c060efc5be8c3f5d6 + args: + - "--env-vars" + - "$(params.ENV_VARS[*])" + script: | + #!/usr/bin/env bash + set -e + + for path in "/tekton/home" "/layers" "$(workspaces.source.path)"; do + echo "> Setting permissions on '$path'..." + chown -R "$(params.USER_ID):$(params.GROUP_ID)" "$path" + done + + echo "> Parsing additional configuration..." + parsing_flag="" + envs=() + for arg in "$@"; do + if [[ "$arg" == "--env-vars" ]]; then + echo "-> Parsing env variables..." + parsing_flag="env-vars" + elif [[ "$parsing_flag" == "env-vars" ]]; then + envs+=("$arg") + fi + done + + echo "> Processing any environment variables..." + ENV_DIR="/platform/env" + + echo "--> Creating 'env' directory: $ENV_DIR" + mkdir -p "$ENV_DIR" + + for env in "${envs[@]}"; do + IFS='=' read -r key value string <<< "$env" + if [[ "$key" != "" && "$value" != "" ]]; then + path="${ENV_DIR}/${key}" + echo "--> Writing ${path}..." + echo -n "$value" > "$path" + fi + done + volumeMounts: + - name: layers-dir + mountPath: /layers + - name: $(params.PLATFORM_DIR) + mountPath: /platform + securityContext: + privileged: true + + - name: create + image: $(params.BUILDER_IMAGE) + imagePullPolicy: Always + command: ["/cnb/lifecycle/creator"] + args: + - "-app=$(workspaces.source.path)/repo$(params.SOURCE_SUBPATH)" + - "-cache-image=$(params.CACHE_IMAGE)" + - "-uid=$(params.USER_ID)" + - "-gid=$(params.GROUP_ID)" + - "-layers=/layers" + - "-platform=/platform" + - "-report=/layers/report.toml" + - "-process-type=$(params.PROCESS_TYPE)" + - "-skip-restore=$(params.SKIP_RESTORE)" + - "-previous-image=$(params.APP_IMAGE)" + - "-run-image=$(params.RUN_IMAGE)" + - "$(params.APP_IMAGE)" + volumeMounts: + - name: layers-dir + mountPath: /layers + - name: $(params.PLATFORM_DIR) + mountPath: /platform + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + + - name: results + image: docker.io/library/bash:5.1.4@sha256:b208215a4655538be652b2769d82e576bc4d0a2bb132144c060efc5be8c3f5d6 + script: | + #!/usr/bin/env bash + set -e + cat /layers/report.toml | grep "digest" | cut -d'"' -f2 | cut -d'"' -f2 | tr -d '\n' | tee $(results.APP_IMAGE_DIGEST.path) + volumeMounts: + - name: layers-dir + mountPath: /layers + + volumes: + - name: empty-dir + emptyDir: {} + - name: layers-dir + emptyDir: {} diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/clone.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/clone.yaml new file mode 100644 index 0000000..ab23d12 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/clone.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: flux-clone + namespace: tekton-runtime +spec: + workspaces: + - name: output + description: The git repo will be cloned onto the volume backing this Workspace. + params: + - name: repository + type: string + - name: revision + type: string + - name: namespace + type: string + - name: dockerHubUsername + type: string + default: "murillodigital" + results: + - name: imageTag + description: "Image repo and tag to push" + steps: + - name: flux-clone + image: ubuntu + script: | + #!/bin/sh + apt-get update + apt-get -y install curl git + curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl + chmod +x ./kubectl + mv ./kubectl /bin/kubectl + mkdir -p ~/.ssh + export SECRET_NAME=$(kubectl get GitRepository $(params.repository) -n $(params.namespace) -o jsonpath='{.spec.secretRef.name}') + export GIT_URL=$(kubectl get GitRepository $(params.repository) -n $(params.namespace) -o jsonpath='{.spec.url}') + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.identity}' | base64 --decode > ~/.ssh/id_rsa + chmod 0400 ~/.ssh/id_rsa + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.identity\.pub}' | base64 --decode > ~/.ssh/id_rsa.pub + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.known_hosts}' | base64 --decode > ~/.ssh/known_hosts + echo "Deleting anything previously checked out" + rm -rf $(workspaces.output.path)/repo + echo "Adding output path as safe directory" + git config --global --add safe.directory $(workspaces.output.path)/repo + echo "Cloning repository" + git clone $GIT_URL $(workspaces.output.path)/repo + cd $(workspaces.output.path)/repo + git fetch + git checkout $(echo "$(params.revision)" | cut -d "/" -f 2) + DOCKERHUB_USERNAME=$(params.dockerHubUsername) + DOCKERHUB_REPOSITORY=$(params.repository) + DOCKERHUB_TAG=$(echo "$(params.revision)" | cut -d "/" -f 2 | awk '{ print substr($0, 0, 6) }') + echo -n "${DOCKERHUB_USERNAME}/${DOCKERHUB_REPOSITORY}:${DOCKERHUB_TAG}" | tee $(results.imageTag.path) +... \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/deploy-gamma.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/deploy-gamma.yaml new file mode 100644 index 0000000..0a83307 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/deploy-gamma.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: deploy-gamma + namespace: tekton-runtime +spec: + workspaces: + - name: source + description: Directory where application source is located. + + params: + - name: APP_IMAGE_DIGEST + description: Image tag to update in gamma overlay + steps: + - name: update-image + image: ubuntu + args: + script: | + #!/usr/bin/env bash + set -e + apt-get update + apt-get -y install curl git wget + curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl + chmod +x ./kubectl + mv ./kubectl /bin/kubectl + mkdir -p ~/.ssh + export SECRET_NAME=$(kubectl get GitRepository $(params.repository) -n $(params.namespace) -o jsonpath='{.spec.secretRef.name}') + export GIT_URL=$(kubectl get GitRepository $(params.repository) -n $(params.namespace) -o jsonpath='{.spec.url}') + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.identity}' | base64 --decode > ~/.ssh/id_rsa + chmod 0400 ~/.ssh/id_rsa + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.identity\.pub}' | base64 --decode > ~/.ssh/id_rsa.pub + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.known_hosts}' | base64 --decode > ~/.ssh/known_hosts + + wget -qO /bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + chmod a+x /bin/yq + + cd $(workspaces.source.path)/repo/src/deploy/overlays/gamma + yq e -i '.spec.template.spec.containers[0].image = "$(params.APP_IMAGE_DIGEST)"' frontend-patch.yaml + + cd cd $(workspaces.source.path)/repo/ + git add -A + git config user.name "Automated Deployment" + git config user.email "automated@weave.works" + git commit -am "Pushing gamma to $(params.APP_IMAGE_DIGEST)" + git push \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/kustomization.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/kustomization.yaml new file mode 100644 index 0000000..fe7e57a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../../base + - buildpack.yaml + - clone.yaml + - scan.yaml + - triggerbinding.yaml + - triggertemplate.yaml + - pipeline.yaml \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/pipeline.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/pipeline.yaml new file mode 100644 index 0000000..d848186 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/pipeline.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: buildpack-build + namespace: tekton-runtime +spec: + params: + - name: repository + type: string + - name: revision + type: string + - name: namespace + type: string + workspaces: + - name: source-workspace + tasks: + - name: fetch-repository + taskRef: + name: flux-clone + workspaces: + - name: output + workspace: source-workspace + params: + - name: repository + value: $(params.repository) + - name: revision + value: $(params.revision) + - name: namespace + value: $(params.namespace) + - name: snyk-scan + runAfter: + - fetch-repository + taskRef: + name: snyk-scan + workspaces: + - name: source + workspace: source-workspace + params: + - name: snyk-token + value: dabe7d01-8f91-42c0-89c1-e86e0351177e + - name: target-directory + value: /examples/gitops-kubernetes-application-pipeline/src + - name: buildpacks + taskRef: + name: buildpacks + runAfter: + - snyk-scan + workspaces: + - name: source + workspace: source-workspace + params: + - name: APP_IMAGE + value: $(tasks.fetch-repository.results.imageTag) + - name: SOURCE_SUBPATH + value: /examples/gitops-kubernetes-application-pipeline/src + - name: BUILDER_IMAGE + value: paketobuildpacks/builder:base + - name: deploy-gamma + taskRef: + name: deploy-gamma + runAfter: + name: buildpacks + workspaces: + - name: source + workspace: source-workspace + params: + - name: APP_IMAGE_DIGEST + value: $(tasks.buildpacks.results.APP_IMAGE_DIGEST) + - name: display-results + runAfter: + - buildpacks + taskSpec: + steps: + - name: print + image: docker.io/library/bash:5.1.4@sha256:b208215a4655538be652b2769d82e576bc4d0a2bb132144c060efc5be8c3f5d6 + script: | + #!/usr/bin/env bash + set -e + echo "Digest of created app image: $(params.DIGEST)" + params: + - name: DIGEST + params: + - name: DIGEST + value: $(tasks.buildpacks.results.APP_IMAGE_DIGEST) diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/scan.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/scan.yaml new file mode 100644 index 0000000..853f883 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/scan.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: snyk-scan + namespace: tekton-runtime + labels: + app.kubernetes.io/version: "0.3" +spec: + workspaces: + - name: source + description: Directory where application source is located. + + params: + - name: snyk-token + description: Token required for Snyk integration. + - name: target-directory + description: Directory to perform snyk scan against + + steps: + - name: scan + image: ubuntu + args: + script: | + #!/usr/bin/env bash + set -e + + apt-get update + apt-get -y install curl golang-go git + + curl https://static.snyk.io/cli/latest/snyk-linux -o snyk + chmod +x ./snyk + mv ./snyk /usr/local/bin/ + + export SNYK_TOKEN=$(params.snyk-token) + + cd $(workspaces.source.path)/repo/$(params.target-directory) + snyk monitor --all-projects \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/triggerbinding.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/triggerbinding.yaml new file mode 100644 index 0000000..df03241 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/triggerbinding.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerBinding +metadata: + name: flux-triggerbinding + namespace: tekton-runtime +spec: + params: + - name: revision + value: $(body.metadata.revision) + - name: repository + value: $(body.involvedObject.name) + - name: namespace + value: $(body.involvedObject.namespace) diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/triggertemplate.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/triggertemplate.yaml new file mode 100644 index 0000000..3cfd309 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/beta/triggertemplate.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerTemplate +metadata: + name: flux-triggertemplate + namespace: tekton-runtime +spec: + params: + - name: revision + - name: repository + - name: namespace + resourcetemplates: + - apiVersion: tekton.dev/v1beta1 + kind: PipelineRun + metadata: + generateName: build-$(tt.params.repository)- + spec: + serviceAccountName: build-pipeline + pipelineRef: + name: buildpack-build + workspaces: + - name: source-workspace + subPath: source + persistentVolumeClaim: + claimName: tekton-pvc + params: + - name: repository + value: '$(tt.params.repository)' + - name: revision + value: '$(tt.params.revision)' + - name: namespace + value: '$(tt.params.namespace)' diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/clone.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/clone.yaml new file mode 100644 index 0000000..e72a81e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/clone.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: flux-clone + namespace: tekton-runtime +spec: + workspaces: + - name: output + description: The git repo will be cloned onto the volume backing this Workspace. + params: + - name: repository + type: string + - name: revision + type: string + - name: namespace + type: string + - name: dockerHubUsername + type: string + default: "murillodigital" + results: + - name: imageTag + description: "Image repo and tag to push" + steps: + - name: flux-clone + image: ubuntu + script: | + #!/bin/sh + apt-get update + apt-get -y install curl git + curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl + chmod +x ./kubectl + mv ./kubectl /bin/kubectl + mkdir -p ~/.ssh + export SECRET_NAME=$(kubectl get GitRepository $(params.repository) -n $(params.namespace) -o jsonpath='{.spec.secretRef.name}') + export GIT_URL=$(kubectl get GitRepository $(params.repository) -n $(params.namespace) -o jsonpath='{.spec.url}') + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.identity}' | base64 --decode > ~/.ssh/id_rsa + chmod 0400 ~/.ssh/id_rsa + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.identity\.pub}' | base64 --decode > ~/.ssh/id_rsa.pub + kubectl get secret $SECRET_NAME -n $(params.namespace) -o jsonpath='{.data.known_hosts}' | base64 --decode > ~/.ssh/known_hosts + echo "Deleting anything previously checked out" + rm -rf $(workspaces.output.path)/repo + echo "Adding output path as safe directory" + git config --global --add safe.directory $(workspaces.output.path)/repo + echo "Cloning repository" + git clone $GIT_URL $(workspaces.output.path)/repo + cd $(workspaces.output.path)/repo + git fetch + git checkout $(echo "$(params.revision)" | cut -d "/" -f 2) +... \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/kustomization.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/kustomization.yaml new file mode 100644 index 0000000..ad571ef --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../../base + - clone.yaml + - scan.yaml + - pipeline.yaml + - triggerbinding.yaml + - triggertemplate.yaml \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/pipeline.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/pipeline.yaml new file mode 100644 index 0000000..741e185 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/pipeline.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: gamma-scan + namespace: tekton-runtime +spec: + params: + - name: repository + type: string + - name: revision + type: string + - name: namespace + type: string + workspaces: + - name: source-workspace + tasks: + - name: fetch-repository + taskRef: + name: flux-clone + workspaces: + - name: output + workspace: source-workspace + params: + - name: repository + value: $(params.repository) + - name: revision + value: $(params.revision) + - name: namespace + value: $(params.namespace) + - name: checkov-scan + runAfter: + - fetch-repository + taskRef: + name: checkov-scan + workspaces: + - name: source + workspace: source-workspace + params: + - name: target-directory + value: /examples/gitops-kubernetes-application-pipeline + \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/scan.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/scan.yaml new file mode 100644 index 0000000..23a33cb --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/scan.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: checkov-scan + namespace: tekton-runtime +spec: + workspaces: + - name: source + description: Directory where application source is located. + params: + - name: target-directory + description: Directory to perform snyk scan against + steps: + - name: echo + image: ubuntu + script: | + #!/bin/sh + apt-get update + apt-get -y install curl git python3 python3-pip + pip3 install checkov + checkov --directory $(workspaces.source.path)/repo/$(params.target-directory) +... \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/triggerbinding.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/triggerbinding.yaml new file mode 100644 index 0000000..df03241 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/triggerbinding.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerBinding +metadata: + name: flux-triggerbinding + namespace: tekton-runtime +spec: + params: + - name: revision + value: $(body.metadata.revision) + - name: repository + value: $(body.involvedObject.name) + - name: namespace + value: $(body.involvedObject.namespace) diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/triggertemplate.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/triggertemplate.yaml new file mode 100644 index 0000000..9c65878 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/pipelines/overlays/gamma/triggertemplate.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerTemplate +metadata: + name: flux-triggertemplate + namespace: tekton-runtime +spec: + params: + - name: revision + - name: repository + - name: namespace + resourcetemplates: + - apiVersion: tekton.dev/v1beta1 + kind: PipelineRun + metadata: + generateName: scan-$(tt.params.repository)- + spec: + serviceAccountName: build-pipeline + pipelineRef: + name: gamma-scan + workspaces: + - name: source-workspace + subPath: source + persistentVolumeClaim: + claimName: tekton-pvc + params: + - name: repository + value: '$(tt.params.repository)' + - name: revision + value: '$(tt.params.revision)' + - name: namespace + value: '$(tt.params.namespace)' diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/deployment.yaml new file mode 100644 index 0000000..c3a5fb8 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: secure +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: backend + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: backend + spec: + serviceAccountName: secure + containers: + - name: backend + image: ghcr.io/stefanprodan/podinfo:5.0.3 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=backend + - --level=info + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 32Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/hpa.yaml new file mode 100644 index 0000000..c84f5b3 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: backend + namespace: secure +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: backend + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/service.yaml new file mode 100644 index 0000000..b69b276 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/backend/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: secure +spec: + type: ClusterIP + selector: + app: backend + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/cluster-issuer.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/cluster-issuer.yaml new file mode 100644 index 0000000..3bdcff1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/cluster-issuer.yaml @@ -0,0 +1,6 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: self-signed +spec: + selfSigned: {} \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/namespace.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/namespace.yaml new file mode 100644 index 0000000..28165dd --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: secure diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/reconciler-rbac.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/reconciler-rbac.yaml new file mode 100644 index 0000000..07087e7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/reconciler-rbac.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: reconciler + namespace: secure +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: reconciler + namespace: secure +rules: + - apiGroups: ['*'] + resources: ['*'] + verbs: ['*'] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: reconciler + namespace: secure +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: reconciler +subjects: + - kind: ServiceAccount + name: reconciler + namespace: secure diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/service-account.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/service-account.yaml new file mode 100644 index 0000000..471fa20 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/common/service-account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: secure + namespace: secure diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/certificate.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/certificate.yaml new file mode 100644 index 0000000..9648dcd --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/certificate.yaml @@ -0,0 +1,15 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: podinfo-frontend + namespace: secure +spec: + dnsNames: + - frontend + - frontend.secure + - frontend.secure.cluster.local + - localhost + secretName: podinfo-frontend-tls + issuerRef: + name: self-signed + kind: ClusterIssuer diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/deployment.yaml new file mode 100644 index 0000000..eacd4c0 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/deployment.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: secure +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend + spec: + serviceAccountName: secure + volumes: + - name: tls + secret: + secretName: podinfo-frontend-tls + containers: + - name: frontend + image: deavon/podinfo:secure-port + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + allowPrivilegeEscalation: true + ports: + - name: http + containerPort: 9898 + protocol: TCP + hostPort: 80 + - name: https + containerPort: 9899 + protocol: TCP + hostPort: 443 + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + volumeMounts: + - name: tls + mountPath: /data/cert + readOnly: true + command: + - ./podinfo + - --port=9898 + - --secure-port=9899 + - --port-metrics=9797 + - --level=info + - --cert-path=/data/cert + - --backend-url=http://backend:9898/echo + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/hpa.yaml new file mode 100644 index 0000000..137606b --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend + namespace: secure +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: 1 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/service.yaml new file mode 100644 index 0000000..95d5e67 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/secure/frontend/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: secure +spec: + type: ClusterIP + selector: + app: frontend + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + - name: https + port: 443 + protocol: TCP + targetPort: https \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/deployment.yaml new file mode 100644 index 0000000..ac6c02e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: webapp +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: backend + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: backend + spec: + serviceAccountName: webapp + containers: + - name: backend + image: ghcr.io/stefanprodan/podinfo:6.3.5 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=backend + - --level=info + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 32Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/hpa.yaml new file mode 100644 index 0000000..76b2210 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: backend + namespace: webapp +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: backend + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/service.yaml new file mode 100644 index 0000000..ef4e7e1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/backend/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: webapp +spec: + type: ClusterIP + selector: + app: backend + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/namespace.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/namespace.yaml new file mode 100644 index 0000000..11ec570 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: webapp diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/reconciler-rbac.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/reconciler-rbac.yaml new file mode 100644 index 0000000..3bc05a7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/reconciler-rbac.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: reconciler + namespace: webapp +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: reconciler + namespace: webapp +rules: + - apiGroups: ['*'] + resources: ['*'] + verbs: ['*'] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: reconciler + namespace: webapp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: reconciler +subjects: + - kind: ServiceAccount + name: reconciler + namespace: webapp diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/service-account.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/service-account.yaml new file mode 100644 index 0000000..12d9d86 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/common/service-account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: webapp + namespace: webapp diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/deployment.yaml new file mode 100644 index 0000000..c3c80c8 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: webapp +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend + spec: + serviceAccountName: webapp + containers: + - name: frontend + image: ghcr.io/stefanprodan/podinfo:6.3.5 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --level=info + - --backend-url=http://backend:9898/echo + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/hpa.yaml new file mode 100644 index 0000000..62cc8f1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend + namespace: webapp +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: backend + minReplicas: 1 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/service.yaml new file mode 100644 index 0000000..3ea7960 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/deploy/webapp/frontend/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: webapp +spec: + type: ClusterIP + selector: + app: frontend + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http diff --git a/examples/gitops-kubernetes-application-pipeline/src/go.mod b/examples/gitops-kubernetes-application-pipeline/src/go.mod new file mode 100644 index 0000000..cb34177 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/go.mod @@ -0,0 +1,87 @@ +module github.com/stefanprodan/podinfo + +go 1.19 + +require ( + github.com/chzyer/readline v1.5.1 + github.com/fatih/color v1.14.1 + github.com/fsnotify/fsnotify v1.6.0 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/gomodule/redigo v1.8.9 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.0 + github.com/prometheus/client_golang v1.14.0 + github.com/spf13/cobra v1.6.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.15.0 + github.com/swaggo/http-swagger v1.3.3 + github.com/swaggo/swag v1.8.10 + go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.40.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 + go.opentelemetry.io/contrib/propagators/aws v1.15.0 + go.opentelemetry.io/contrib/propagators/b3 v1.15.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 + go.opentelemetry.io/contrib/propagators/ot v1.15.0 + go.opentelemetry.io/otel v1.14.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0 + go.opentelemetry.io/otel/sdk v1.14.0 + go.opentelemetry.io/otel/trace v1.14.0 + go.uber.org/zap v1.24.0 + golang.org/x/net v0.8.0 + google.golang.org/grpc v1.53.0 +) + +// Fix CVE-2022-32149 +replace golang.org/x/text => golang.org/x/text v0.7.0 + +// Fix CVE-2022-28948 +replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect + go.opentelemetry.io/otel/metric v0.37.0 // indirect + go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.1.12 // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/gitops-kubernetes-application-pipeline/src/go.sum b/examples/gitops-kubernetes-application-pipeline/src/go.sum new file mode 100644 index 0000000..080dab6 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/go.sum @@ -0,0 +1,736 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc= +github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo= +github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= +github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.40.0 h1:KToMJH0+5VxWBGtfeluRmWR3wLtE7nP+80YrxNI5FGs= +go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.40.0/go.mod h1:RK3vgddjxVcF1q7IBVppzG6k2cW/NBnZHQ3X4g+EYBQ= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 h1:ZjF6qLnAVNq6xUh0sK2mCEqwnRrpgr0mLALQXJL34NI= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0/go.mod h1:SD34NWTW0VMH2VvFVfArHPoF+L1ddT4MOQCTb2l8T5I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 h1:lE9EJyw3/JhrjWH/hEy9FptnalDQgj7vpbgC2KCCCxE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0/go.mod h1:pcQ3MM3SWvrA71U4GDqv9UFDJ3HQsW7y5ZO3tDTlUdI= +go.opentelemetry.io/contrib/propagators/aws v1.15.0 h1:FLe+bRTMAhEALItDQt1U2S/rdq8/rGGJTJpOpCDvMu0= +go.opentelemetry.io/contrib/propagators/aws v1.15.0/go.mod h1:Z/nqdjqKjErrS3gYoEMZt8//dt8VZbqalD0V+7vh7lM= +go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= +go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= +go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 h1:xdJjwy5t/8I+TZehMMQ+r2h50HREihH2oMUhimQ+jug= +go.opentelemetry.io/contrib/propagators/jaeger v1.15.0/go.mod h1:tU0nwW4QTvKceNUP60/PQm0FI8zDSwey7gIFt3RR/yw= +go.opentelemetry.io/contrib/propagators/ot v1.15.0 h1:iBNejawWy7wWZ5msuZDNcMjBy14Wc0v3gCAXukGHN/Q= +go.opentelemetry.io/contrib/propagators/ot v1.15.0/go.mod h1:0P7QQ+MHt6SXR1ATaMpewSiWlp8NbKErNLKcaU4EEKI= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 h1:/fXHZHGvro6MVqV34fJzDhi7sHGpX3Ej/Qjmfn003ho= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0/go.mod h1:UFG7EBMRdXyFstOwH028U0sVf+AvukSGhF0g8+dmNG8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 h1:TKf2uAs2ueguzLaxOCBXNpHxfO/aC7PAdDsSH0IbeRQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0/go.mod h1:HrbCVv40OOLTABmOn1ZWty6CHXkU8DK/Urc43tHug70= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0 h1:ap+y8RXX3Mu9apKVtOkM6WSFESLM8K3wNQyOU8sWHcc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0/go.mod h1:5w41DY6S9gZrbjuq6Y+753e96WfPha5IcsOSZTtullM= +go.opentelemetry.io/otel/metric v0.37.0 h1:pHDQuLQOZwYD+Km0eb657A25NaRzy0a+eLyKfDXedEs= +go.opentelemetry.io/otel/metric v0.37.0/go.mod h1:DmdaHfGt54iV6UKxsV9slj2bBRJcKC1B1uvDLIioc1s= +go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= +go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/examples/gitops-kubernetes-application-pipeline/src/kustomize/deployment.yaml b/examples/gitops-kubernetes-application-pipeline/src/kustomize/deployment.yaml new file mode 100644 index 0000000..f0f5138 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/kustomize/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.3.5 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/examples/gitops-kubernetes-application-pipeline/src/kustomize/hpa.yaml b/examples/gitops-kubernetes-application-pipeline/src/kustomize/hpa.yaml new file mode 100644 index 0000000..263e912 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/kustomize/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 diff --git a/examples/gitops-kubernetes-application-pipeline/src/kustomize/kustomization.yaml b/examples/gitops-kubernetes-application-pipeline/src/kustomize/kustomization.yaml new file mode 100644 index 0000000..470e464 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/kustomize/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - hpa.yaml + - deployment.yaml + - service.yaml + diff --git a/examples/gitops-kubernetes-application-pipeline/src/kustomize/service.yaml b/examples/gitops-kubernetes-application-pipeline/src/kustomize/service.yaml new file mode 100644 index 0000000..9450823 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/kustomize/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: podinfo +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/examples/gitops-kubernetes-application-pipeline/src/otel/Makefile b/examples/gitops-kubernetes-application-pipeline/src/otel/Makefile new file mode 100644 index 0000000..0b9410a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/otel/Makefile @@ -0,0 +1,20 @@ +DC=docker-compose -f docker-compose.yaml + +.PHONY: help +.DEFAULT_GOAL := help + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +stop: ## Stop all Docker Containers run in Compose + $(DC) stop + +clean: stop ## Clean all Docker Containers and Volumes + $(DC) down --rmi local --remove-orphans -v + $(DC) rm -f -v + +build: clean ## Rebuild the Docker Image for use by Compose + $(DC) build + +run: stop ## Run the Application + $(DC) up diff --git a/examples/gitops-kubernetes-application-pipeline/src/otel/README.md b/examples/gitops-kubernetes-application-pipeline/src/otel/README.md new file mode 100644 index 0000000..8a23f58 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/otel/README.md @@ -0,0 +1,37 @@ +# Tracing Demo + +The directory contains sample [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) +and [Jaeger](https://www.jaegertracing.io) configurations for a tracing demo. + +## Configuration + +The provided [docker-compose.yaml](docker-compose.yaml) sets up 4 Containers + +1. PodInfo Frontend on port 9898 +2. PodInfo Backend on port 9899 +3. OpenTelemetry Collector listening on port 4317 for GRPC +4. Jaeger all-in-one listening on multiple ports + +## How does it work? + +The frontend pods are configured to call onto the backend pods. Both the podinfo +pods are configured to send traces over to the collector at port 4317 using GRPC. +The collector forwards all received spans to Jaeger over port 14250 and Jaeger +exposes a UI over port `16686`. + +## Running it locally + +1. Start all the Containers +```shell +make run +``` +2. Send some sample requests +```shell +curl -v http://localhost:9898/status/200 +curl -X POST -v http://localhost:9898/api/echo +``` +3. Visit `http://localhost:16686/` to see the spans +4. Stop all the containers +```shell +make stop +``` diff --git a/examples/gitops-kubernetes-application-pipeline/src/otel/docker-compose.yaml b/examples/gitops-kubernetes-application-pipeline/src/otel/docker-compose.yaml new file mode 100644 index 0000000..ab9b56b --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/otel/docker-compose.yaml @@ -0,0 +1,35 @@ +version: '2' + +services: + podinfo_frontend: + build: .. + command: ./podinfo --backend-url http://podinfo_backend:9899/status/200 --otel-service-name=podinfo_frontend + environment: + - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel:4317 + ports: + - "9898:9898" + podinfo_backend: + build: .. + command: ./podinfo --port 9899 --otel-service-name=podinfo_backend + environment: + - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel:4317 + ports: + - "9899:9899" + otel: + command: --config otel-config.yaml + image: otel/opentelemetry-collector:0.41.0 + ports: + - "4317:4317" + volumes: + - ${PWD}/otel-config.yaml:/otel-config.yaml + jaeger: + image: jaegertracing/all-in-one:1.29.0 + ports: + - "5775:5775/udp" + - "6831:6831/udp" + - "6832:6832/udp" + - "5778:5778" + - "16686:16686" + - "14268:14268" + - "14250:14250" + - "9411:9411" diff --git a/examples/gitops-kubernetes-application-pipeline/src/otel/otel-config.yaml b/examples/gitops-kubernetes-application-pipeline/src/otel/otel-config.yaml new file mode 100644 index 0000000..f123d4d --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/otel/otel-config.yaml @@ -0,0 +1,26 @@ +receivers: + otlp: + protocols: + grpc: + http: + +processors: + +exporters: + jaeger: + endpoint: jaeger:14250 + tls: + insecure: true + +extensions: + health_check: + pprof: + zpages: + +service: + extensions: [health_check,pprof,zpages] + pipelines: + traces: + receivers: [otlp] + processors: [] + exporters: [jaeger] diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/cache.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/cache.go new file mode 100644 index 0000000..ac67873 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/cache.go @@ -0,0 +1,177 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/gomodule/redigo/redis" + "github.com/gorilla/mux" + "go.uber.org/zap" + + "github.com/stefanprodan/podinfo/pkg/version" +) + +// Cache godoc +// @Summary Save payload in cache +// @Description writes the posted content in cache +// @Tags HTTP API +// @Accept json +// @Produce json +// @Param key path string true "Key to save to" +// @Router /cache/{key} [post] +// @Success 202 +func (s *Server) cacheWriteHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "cacheWriteHandler") + defer span.End() + + if s.pool == nil { + s.ErrorResponse(w, r, span, "cache server is offline", http.StatusBadRequest) + return + } + + key := mux.Vars(r)["key"] + body, err := io.ReadAll(r.Body) + if err != nil { + s.ErrorResponse(w, r, span, "reading the request body failed", http.StatusBadRequest) + return + } + + conn := s.pool.Get() + defer conn.Close() + _, err = conn.Do("SET", key, string(body)) + if err != nil { + s.logger.Warn("cache set failed", zap.Error(err)) + s.ErrorResponse(w, r, span, "cache set failed", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +// Cache godoc +// @Summary Delete payload from cache +// @Description deletes the key and its value from cache +// @Tags HTTP API +// @Accept json +// @Produce json +// @Param key path string true "Key to delete" +// @Router /cache/{key} [delete] +// @Success 202 +func (s *Server) cacheDeleteHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "cacheDeleteHandler") + defer span.End() + + if s.pool == nil { + s.ErrorResponse(w, r, span, "cache server is offline", http.StatusBadRequest) + return + } + + key := mux.Vars(r)["key"] + + conn := s.pool.Get() + defer conn.Close() + _, err := conn.Do("DEL", key) + if err != nil { + s.logger.Warn("cache delete failed", zap.Error(err)) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +// Cache godoc +// @Summary Get payload from cache +// @Description returns the content from cache if key exists +// @Tags HTTP API +// @Accept json +// @Produce json +// @Param key path string true "Key to load from cache" +// @Router /cache/{key} [get] +// @Success 200 {string} string value +func (s *Server) cacheReadHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "cacheReadHandler") + defer span.End() + + if s.pool == nil { + s.ErrorResponse(w, r, span, "cache server is offline", http.StatusBadRequest) + return + } + + key := mux.Vars(r)["key"] + + conn := s.pool.Get() + defer conn.Close() + + ok, err := redis.Bool(conn.Do("EXISTS", key)) + if err != nil || !ok { + s.logger.Warn("cache key not found", zap.String("key", key)) + w.WriteHeader(http.StatusNotFound) + return + } + + data, err := redis.String(conn.Do("GET", key)) + if err != nil { + s.logger.Warn("cache get failed", zap.Error(err)) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(data)) +} + +func (s *Server) getCacheConn() (redis.Conn, error) { + redisUrl, err := url.Parse(s.config.CacheServer) + if err != nil { + return nil, fmt.Errorf("failed to parse redis url: %v", err) + } + + var opts []redis.DialOption + if user := redisUrl.User; user != nil { + opts = append(opts, redis.DialUsername(user.Username())) + if password, ok := user.Password(); ok { + opts = append(opts, redis.DialPassword(password)) + } + } + + return redis.Dial("tcp", redisUrl.Host, opts...) +} + +func (s *Server) startCachePool(ticker *time.Ticker) { + if s.config.CacheServer == "" { + return + } + s.pool = &redis.Pool{ + MaxIdle: 3, + IdleTimeout: 240 * time.Second, + Dial: s.getCacheConn, + TestOnBorrow: func(c redis.Conn, t time.Time) error { + _, err := c.Do("PING") + return err + }, + } + + // set = with an expiry time of one minute + setVersion := func() { + conn := s.pool.Get() + if _, err := conn.Do("SET", s.config.Hostname, version.VERSION, "EX", 60); err != nil { + s.logger.Warn("cache server is offline", zap.Error(err), zap.String("server", s.config.CacheServer)) + } + _ = conn.Close() + } + + // set version on a schedule + go func() { + setVersion() + for { + select { + case <-ticker.C: + setVersion() + } + } + }() +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/chunked.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/chunked.go new file mode 100644 index 0000000..0bf668e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/chunked.go @@ -0,0 +1,48 @@ +package api + +import ( + "math/rand" + "net/http" + "strconv" + "time" + + "github.com/gorilla/mux" +) + +// Chunked godoc +// @Summary Chunked transfer encoding +// @Description uses transfer-encoding type chunked to give a partial response and then waits for the specified period +// @Tags HTTP API +// @Accept json +// @Produce json +// @Param seconds path int true "seconds to wait for" +// @Router /chunked/{seconds} [get] +// @Success 200 {object} api.MapResponse +func (s *Server) chunkedHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "chunkedHandler") + defer span.End() + + vars := mux.Vars(r) + + delay, err := strconv.Atoi(vars["wait"]) + if err != nil { + delay = rand.Intn(int(s.config.HttpServerTimeout*time.Second)-10) + 10 + } + + flusher, ok := w.(http.Flusher) + if !ok { + s.ErrorResponse(w, r, span, "Streaming unsupported!", http.StatusInternalServerError) + return + } + + w.Header().Set("Connection", "Keep-Alive") + w.Header().Set("Transfer-Encoding", "chunked") + w.Header().Set("X-Content-Type-Options", "nosniff") + + flusher.Flush() + + time.Sleep(time.Duration(delay) * time.Second) + s.JSONResponse(w, r, map[string]int{"delay": delay}) + + flusher.Flush() +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/chunked_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/chunked_test.go new file mode 100644 index 0000000..090dd2f --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/chunked_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +func TestChunkedHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/chunked/0", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + + srv.router.HandleFunc("/chunked/{wait}", srv.chunkedHandler) + srv.router.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := ".*delay.*0.*" + r := regexp.MustCompile(expected) + if !r.MatchString(rr.Body.String()) { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/configs.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/configs.go new file mode 100644 index 0000000..9b08415 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/configs.go @@ -0,0 +1,18 @@ +package api + +import "net/http" + +func (s *Server) configReadHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "configReadHandler") + defer span.End() + + files := make(map[string]string) + if watcher != nil { + watcher.Cache.Range(func(key interface{}, value interface{}) bool { + files[key.(string)] = value.(string) + return true + }) + } + + s.JSONResponse(w, r, files) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/delay.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/delay.go new file mode 100644 index 0000000..6655cf7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/delay.go @@ -0,0 +1,70 @@ +package api + +import ( + "math/rand" + "net/http" + + "strconv" + "time" + + "github.com/gorilla/mux" +) + +type RandomDelayMiddleware struct { + min int + max int + unit string +} + +func NewRandomDelayMiddleware(minDelay, maxDelay int, delayUnit string) *RandomDelayMiddleware { + return &RandomDelayMiddleware{ + min: minDelay, + max: maxDelay, + unit: delayUnit, + } +} + +func (m *RandomDelayMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var unit time.Duration + rand.Seed(time.Now().Unix()) + switch m.unit { + case "s": + unit = time.Second + case "ms": + unit = time.Millisecond + default: + unit = time.Second + } + + delay := rand.Intn(m.max-m.min) + m.min + time.Sleep(time.Duration(delay) * unit) + next.ServeHTTP(w, r) + }) +} + +// Delay godoc +// @Summary Delay +// @Description waits for the specified period +// @Tags HTTP API +// @Accept json +// @Produce json +// @Param seconds path int true "seconds to wait for" +// @Router /delay/{seconds} [get] +// @Success 200 {object} api.MapResponse +func (s *Server) delayHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "delayHandler") + defer span.End() + + vars := mux.Vars(r) + + delay, err := strconv.Atoi(vars["wait"]) + if err != nil { + s.ErrorResponse(w, r, span, err.Error(), http.StatusBadRequest) + return + } + + time.Sleep(time.Duration(delay) * time.Second) + + s.JSONResponse(w, r, map[string]int{"delay": delay}) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/delay_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/delay_test.go new file mode 100644 index 0000000..88d5ab7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/delay_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +func TestDelayHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/delay/0", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + + srv.router.HandleFunc("/delay/{wait}", srv.delayHandler) + srv.router.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := ".*delay.*0.*" + r := regexp.MustCompile(expected) + if !r.MatchString(rr.Body.String()) { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/docs.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/docs.go new file mode 100644 index 0000000..cc0af30 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/docs.go @@ -0,0 +1,683 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Source Code", + "url": "https://github.com/stefanprodan/podinfo" + }, + "license": { + "name": "MIT License", + "url": "https://github.com/stefanprodan/podinfo/blob/master/LICENSE" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/": { + "get": { + "description": "renders podinfo UI", + "produces": [ + "text/html" + ], + "tags": [ + "HTTP API" + ], + "summary": "Index", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/echo": { + "post": { + "description": "forwards the call to the backend service and echos the posted content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Echo", + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/api/info": { + "get": { + "description": "returns the runtime information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Runtime information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.RuntimeResponse" + } + } + } + } + }, + "/cache/{key}": { + "get": { + "description": "returns the content from cache if key exists", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Get payload from cache", + "parameters": [ + { + "type": "string", + "description": "Key to load from cache", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "writes the posted content in cache", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Save payload in cache", + "parameters": [ + { + "type": "string", + "description": "Key to save to", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + }, + "delete": { + "description": "deletes the key and its value from cache", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Delete payload from cache", + "parameters": [ + { + "type": "string", + "description": "Key to delete", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/chunked/{seconds}": { + "get": { + "description": "uses transfer-encoding type chunked to give a partial response and then waits for the specified period", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Chunked transfer encoding", + "parameters": [ + { + "type": "integer", + "description": "seconds to wait for", + "name": "seconds", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/delay/{seconds}": { + "get": { + "description": "waits for the specified period", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Delay", + "parameters": [ + { + "type": "integer", + "description": "seconds to wait for", + "name": "seconds", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/env": { + "get": { + "description": "returns the environment variables as a JSON array", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Environment", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/headers": { + "get": { + "description": "returns a JSON array with the request HTTP headers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Headers", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/healthz": { + "get": { + "description": "used by Kubernetes liveness probe", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Liveness check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/metrics": { + "get": { + "description": "returns HTTP requests duration and Go runtime metrics", + "produces": [ + "text/plain" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Prometheus metrics", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/panic": { + "get": { + "description": "crashes the process with exit code 255", + "tags": [ + "HTTP API" + ], + "summary": "Panic", + "responses": {} + } + }, + "/readyz": { + "get": { + "description": "used by Kubernetes readiness probe", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Readiness check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/readyz/disable": { + "post": { + "description": "signals the Kubernetes LB to stop sending requests to this instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Disable ready state", + "responses": { + "202": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/readyz/enable": { + "post": { + "description": "signals the Kubernetes LB that this instance is ready to receive traffic", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Enable ready state", + "responses": { + "202": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/status/{code}": { + "get": { + "description": "sets the response status code to the specified code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Status code", + "parameters": [ + { + "type": "integer", + "description": "status code to return", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/store": { + "post": { + "description": "writes the posted content to disk at /data/hash and returns the SHA1 hash of the content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Upload file", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/store/{hash}": { + "get": { + "description": "returns the content of the file /data/hash if exists", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "HTTP API" + ], + "summary": "Download file", + "parameters": [ + { + "type": "string", + "description": "hash value", + "name": "hash", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "file", + "schema": { + "type": "string" + } + } + } + } + }, + "/token": { + "post": { + "description": "issues a JWT token valid for one minute", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Generate JWT token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TokenResponse" + } + } + } + } + }, + "/token/validate": { + "post": { + "description": "validates the JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Validate JWT token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TokenValidationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/version": { + "get": { + "description": "returns podinfo version and git commit hash", + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Version", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/ws/echo": { + "post": { + "description": "echos content via websockets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Echo over websockets", + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + } + }, + "definitions": { + "api.MapResponse": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "api.RuntimeResponse": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "goarch": { + "type": "string" + }, + "goos": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "message": { + "type": "string" + }, + "num_cpu": { + "type": "string" + }, + "num_goroutine": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "runtime": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "api.TokenResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "api.TokenValidationResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "token_name": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "2.0", + Host: "localhost:9898", + BasePath: "/", + Schemes: []string{"http", "https"}, + Title: "Podinfo API", + Description: "Go microservice template for Kubernetes.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/swagger.json b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/swagger.json new file mode 100644 index 0000000..063d17d --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/swagger.json @@ -0,0 +1,664 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "Go microservice template for Kubernetes.", + "title": "Podinfo API", + "contact": { + "name": "Source Code", + "url": "https://github.com/stefanprodan/podinfo" + }, + "license": { + "name": "MIT License", + "url": "https://github.com/stefanprodan/podinfo/blob/master/LICENSE" + }, + "version": "2.0" + }, + "host": "localhost:9898", + "basePath": "/", + "paths": { + "/": { + "get": { + "description": "renders podinfo UI", + "produces": [ + "text/html" + ], + "tags": [ + "HTTP API" + ], + "summary": "Index", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/echo": { + "post": { + "description": "forwards the call to the backend service and echos the posted content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Echo", + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/api/info": { + "get": { + "description": "returns the runtime information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Runtime information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.RuntimeResponse" + } + } + } + } + }, + "/cache/{key}": { + "get": { + "description": "returns the content from cache if key exists", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Get payload from cache", + "parameters": [ + { + "type": "string", + "description": "Key to load from cache", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "writes the posted content in cache", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Save payload in cache", + "parameters": [ + { + "type": "string", + "description": "Key to save to", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + }, + "delete": { + "description": "deletes the key and its value from cache", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Delete payload from cache", + "parameters": [ + { + "type": "string", + "description": "Key to delete", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/chunked/{seconds}": { + "get": { + "description": "uses transfer-encoding type chunked to give a partial response and then waits for the specified period", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Chunked transfer encoding", + "parameters": [ + { + "type": "integer", + "description": "seconds to wait for", + "name": "seconds", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/delay/{seconds}": { + "get": { + "description": "waits for the specified period", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Delay", + "parameters": [ + { + "type": "integer", + "description": "seconds to wait for", + "name": "seconds", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/env": { + "get": { + "description": "returns the environment variables as a JSON array", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Environment", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/headers": { + "get": { + "description": "returns a JSON array with the request HTTP headers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Headers", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/healthz": { + "get": { + "description": "used by Kubernetes liveness probe", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Liveness check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/metrics": { + "get": { + "description": "returns HTTP requests duration and Go runtime metrics", + "produces": [ + "text/plain" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Prometheus metrics", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/panic": { + "get": { + "description": "crashes the process with exit code 255", + "tags": [ + "HTTP API" + ], + "summary": "Panic", + "responses": {} + } + }, + "/readyz": { + "get": { + "description": "used by Kubernetes readiness probe", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Readiness check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/readyz/disable": { + "post": { + "description": "signals the Kubernetes LB to stop sending requests to this instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Disable ready state", + "responses": { + "202": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/readyz/enable": { + "post": { + "description": "signals the Kubernetes LB that this instance is ready to receive traffic", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Enable ready state", + "responses": { + "202": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/status/{code}": { + "get": { + "description": "sets the response status code to the specified code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Status code", + "parameters": [ + { + "type": "integer", + "description": "status code to return", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/store": { + "post": { + "description": "writes the posted content to disk at /data/hash and returns the SHA1 hash of the content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Upload file", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/store/{hash}": { + "get": { + "description": "returns the content of the file /data/hash if exists", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "HTTP API" + ], + "summary": "Download file", + "parameters": [ + { + "type": "string", + "description": "hash value", + "name": "hash", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "file", + "schema": { + "type": "string" + } + } + } + } + }, + "/token": { + "post": { + "description": "issues a JWT token valid for one minute", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Generate JWT token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TokenResponse" + } + } + } + } + }, + "/token/validate": { + "post": { + "description": "validates the JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Validate JWT token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TokenValidationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/version": { + "get": { + "description": "returns podinfo version and git commit hash", + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Version", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + }, + "/ws/echo": { + "post": { + "description": "echos content via websockets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "HTTP API" + ], + "summary": "Echo over websockets", + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/api.MapResponse" + } + } + } + } + } + }, + "definitions": { + "api.MapResponse": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "api.RuntimeResponse": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "goarch": { + "type": "string" + }, + "goos": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "message": { + "type": "string" + }, + "num_cpu": { + "type": "string" + }, + "num_goroutine": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "runtime": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "api.TokenResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "api.TokenValidationResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "token_name": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/swagger.yaml b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/swagger.yaml new file mode 100644 index 0000000..3c21779 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/docs/swagger.yaml @@ -0,0 +1,439 @@ +basePath: / +definitions: + api.MapResponse: + additionalProperties: + type: string + type: object + api.RuntimeResponse: + properties: + color: + type: string + goarch: + type: string + goos: + type: string + hostname: + type: string + logo: + type: string + message: + type: string + num_cpu: + type: string + num_goroutine: + type: string + revision: + type: string + runtime: + type: string + version: + type: string + type: object + api.TokenResponse: + properties: + expires_at: + type: string + token: + type: string + type: object + api.TokenValidationResponse: + properties: + expires_at: + type: string + token_name: + type: string + type: object +host: localhost:9898 +info: + contact: + name: Source Code + url: https://github.com/stefanprodan/podinfo + description: Go microservice template for Kubernetes. + license: + name: MIT License + url: https://github.com/stefanprodan/podinfo/blob/master/LICENSE + title: Podinfo API + version: "2.0" +paths: + /: + get: + description: renders podinfo UI + produces: + - text/html + responses: + "200": + description: OK + schema: + type: string + summary: Index + tags: + - HTTP API + /api/echo: + post: + consumes: + - application/json + description: forwards the call to the backend service and echos the posted content + produces: + - application/json + responses: + "202": + description: Accepted + schema: + $ref: '#/definitions/api.MapResponse' + summary: Echo + tags: + - HTTP API + /api/info: + get: + consumes: + - application/json + description: returns the runtime information + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.RuntimeResponse' + summary: Runtime information + tags: + - HTTP API + /cache/{key}: + delete: + consumes: + - application/json + description: deletes the key and its value from cache + parameters: + - description: Key to delete + in: path + name: key + required: true + type: string + produces: + - application/json + responses: + "202": + description: Accepted + summary: Delete payload from cache + tags: + - HTTP API + get: + consumes: + - application/json + description: returns the content from cache if key exists + parameters: + - description: Key to load from cache + in: path + name: key + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: Get payload from cache + tags: + - HTTP API + post: + consumes: + - application/json + description: writes the posted content in cache + parameters: + - description: Key to save to + in: path + name: key + required: true + type: string + produces: + - application/json + responses: + "202": + description: Accepted + summary: Save payload in cache + tags: + - HTTP API + /chunked/{seconds}: + get: + consumes: + - application/json + description: uses transfer-encoding type chunked to give a partial response + and then waits for the specified period + parameters: + - description: seconds to wait for + in: path + name: seconds + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MapResponse' + summary: Chunked transfer encoding + tags: + - HTTP API + /delay/{seconds}: + get: + consumes: + - application/json + description: waits for the specified period + parameters: + - description: seconds to wait for + in: path + name: seconds + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MapResponse' + summary: Delay + tags: + - HTTP API + /env: + get: + consumes: + - application/json + description: returns the environment variables as a JSON array + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + summary: Environment + tags: + - HTTP API + /headers: + get: + consumes: + - application/json + description: returns a JSON array with the request HTTP headers + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + summary: Headers + tags: + - HTTP API + /healthz: + get: + consumes: + - application/json + description: used by Kubernetes liveness probe + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: Liveness check + tags: + - Kubernetes + /metrics: + get: + description: returns HTTP requests duration and Go runtime metrics + produces: + - text/plain + responses: + "200": + description: OK + schema: + type: string + summary: Prometheus metrics + tags: + - Kubernetes + /panic: + get: + description: crashes the process with exit code 255 + responses: {} + summary: Panic + tags: + - HTTP API + /readyz: + get: + consumes: + - application/json + description: used by Kubernetes readiness probe + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: Readiness check + tags: + - Kubernetes + /readyz/disable: + post: + consumes: + - application/json + description: signals the Kubernetes LB to stop sending requests to this instance + produces: + - application/json + responses: + "202": + description: OK + schema: + type: string + summary: Disable ready state + tags: + - Kubernetes + /readyz/enable: + post: + consumes: + - application/json + description: signals the Kubernetes LB that this instance is ready to receive + traffic + produces: + - application/json + responses: + "202": + description: OK + schema: + type: string + summary: Enable ready state + tags: + - Kubernetes + /status/{code}: + get: + consumes: + - application/json + description: sets the response status code to the specified code + parameters: + - description: status code to return + in: path + name: code + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MapResponse' + summary: Status code + tags: + - HTTP API + /store: + post: + consumes: + - application/json + description: writes the posted content to disk at /data/hash and returns the + SHA1 hash of the content + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MapResponse' + summary: Upload file + tags: + - HTTP API + /store/{hash}: + get: + consumes: + - application/json + description: returns the content of the file /data/hash if exists + parameters: + - description: hash value + in: path + name: hash + required: true + type: string + produces: + - text/plain + responses: + "200": + description: file + schema: + type: string + summary: Download file + tags: + - HTTP API + /token: + post: + consumes: + - application/json + description: issues a JWT token valid for one minute + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TokenResponse' + summary: Generate JWT token + tags: + - HTTP API + /token/validate: + post: + consumes: + - application/json + description: validates the JWT token + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TokenValidationResponse' + "401": + description: Unauthorized + schema: + type: string + summary: Validate JWT token + tags: + - HTTP API + /version: + get: + description: returns podinfo version and git commit hash + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MapResponse' + summary: Version + tags: + - HTTP API + /ws/echo: + post: + consumes: + - application/json + description: echos content via websockets + produces: + - application/json + responses: + "202": + description: Accepted + schema: + $ref: '#/definitions/api.MapResponse' + summary: Echo over websockets + tags: + - HTTP API +schemes: +- http +- https +swagger: "2.0" diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echo.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echo.go new file mode 100644 index 0000000..5bf7d8f --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echo.go @@ -0,0 +1,128 @@ +package api + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/http/httptrace" + "sync" + + "github.com/stefanprodan/podinfo/pkg/version" + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.uber.org/zap" +) + +// Echo godoc +// @Summary Echo +// @Description forwards the call to the backend service and echos the posted content +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /api/echo [post] +// @Success 202 {object} api.MapResponse +func (s *Server) echoHandler(w http.ResponseWriter, r *http.Request) { + ctx, span := s.tracer.Start(r.Context(), "echoHandler") + defer span.End() + + body, err := io.ReadAll(r.Body) + if err != nil { + s.logger.Error("reading the request body failed", zap.Error(err)) + s.ErrorResponse(w, r, span, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} + + if len(s.config.BackendURL) > 0 { + result := make([]string, len(s.config.BackendURL)) + var wg sync.WaitGroup + wg.Add(len(s.config.BackendURL)) + for i, b := range s.config.BackendURL { + go func(index int, backend string) { + defer wg.Done() + + ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx)) + ctx, cancel := context.WithTimeout(ctx, s.config.HttpClientTimeout) + defer cancel() + + backendReq, err := http.NewRequestWithContext(ctx, "POST", backend, bytes.NewReader(body)) + if err != nil { + s.logger.Error("backend call failed", zap.Error(err), zap.String("url", backend)) + return + } + + // forward headers + copyTracingHeaders(r, backendReq) + + backendReq.Header.Set("X-API-Version", version.VERSION) + backendReq.Header.Set("X-API-Revision", version.REVISION) + + // call backend + resp, err := client.Do(backendReq) + if err != nil { + s.logger.Error("backend call failed", zap.Error(err), zap.String("url", backend)) + result[index] = fmt.Sprintf("backend %v call failed %v", backend, err) + return + } + defer resp.Body.Close() + + // copy error status from backend and exit + if resp.StatusCode >= 400 { + s.logger.Error("backend call failed", zap.Int("status", resp.StatusCode), zap.String("url", backend)) + result[index] = fmt.Sprintf("backend %v response status code %v", backend, resp.StatusCode) + return + } + + // forward the received body + rbody, err := io.ReadAll(resp.Body) + if err != nil { + s.logger.Error( + "reading the backend request body failed", + zap.Error(err), + zap.String("url", backend)) + result[index] = fmt.Sprintf("backend %v call failed %v", backend, err) + return + } + + s.logger.Debug( + "payload received from backend", + zap.String("response", string(rbody)), + zap.String("url", backend)) + + result[index] = string(rbody) + }(i, b) + } + wg.Wait() + + w.Header().Set("X-Color", s.config.UIColor) + s.JSONResponse(w, r, result) + + } else { + w.Header().Set("X-Color", s.config.UIColor) + w.WriteHeader(http.StatusAccepted) + w.Write(body) + } +} + +func copyTracingHeaders(from *http.Request, to *http.Request) { + headers := []string{ + "x-request-id", + "x-b3-traceid", + "x-b3-spanid", + "x-b3-parentspanid", + "x-b3-sampled", + "x-b3-flags", + "x-ot-span-context", + } + + for i := range headers { + headerValue := from.Header.Get(headers[i]) + if len(headerValue) > 0 { + to.Header.Set(headers[i], headerValue) + } + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echo_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echo_test.go new file mode 100644 index 0000000..a37da8e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echo_test.go @@ -0,0 +1,34 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestEchoHandler(t *testing.T) { + expected := `{"test": true}` + req, err := http.NewRequest("POST", "/api/echo", strings.NewReader(expected)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.echoHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusAccepted { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusAccepted) + } + + // Check the response body is what we expect. + if rr.Body.String() != expected { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echows.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echows.go new file mode 100644 index 0000000..bcf693c --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/echows.go @@ -0,0 +1,90 @@ +package api + +import ( + "net/http" + "strings" + "time" + + "github.com/gorilla/websocket" + "go.uber.org/zap" +) + +var wsCon = websocket.Upgrader{} + +// EchoWS godoc +// @Summary Echo over websockets +// @Description echos content via websockets +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /ws/echo [post] +// @Success 202 {object} api.MapResponse +// Test: go run ./cmd/podcli/* ws localhost:9898/ws/echo +func (s *Server) echoWsHandler(w http.ResponseWriter, r *http.Request) { + c, err := wsCon.Upgrade(w, r, nil) + if err != nil { + if err != nil { + s.logger.Warn("websocket upgrade error", zap.Error(err)) + return + } + } + defer c.Close() + done := make(chan struct{}) + defer close(done) + in := make(chan interface{}) + defer close(in) + go s.writeWs(c, in) + go s.sendHostWs(c, in, done) + for { + _, message, err := c.ReadMessage() + if err != nil { + if !strings.Contains(err.Error(), "close") { + s.logger.Warn("websocket read error", zap.Error(err)) + } + break + } + var response = struct { + Time time.Time `json:"ts"` + Message string `json:"msg"` + }{ + Time: time.Now(), + Message: string(message), + } + in <- response + } +} + +func (s *Server) sendHostWs(ws *websocket.Conn, in chan interface{}, done chan struct{}) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + var status = struct { + Time time.Time `json:"ts"` + Host string `json:"server"` + }{ + Time: time.Now(), + Host: s.config.Hostname, + } + in <- status + case <-done: + s.logger.Debug("websocket exit") + return + } + } +} + +func (s *Server) writeWs(ws *websocket.Conn, in chan interface{}) { + for { + select { + case msg := <-in: + if err := ws.WriteJSON(msg); err != nil { + if !strings.Contains(err.Error(), "close") { + s.logger.Warn("websocket write error", zap.Error(err)) + } + return + } + } + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/env.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/env.go new file mode 100644 index 0000000..c9951cc --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/env.go @@ -0,0 +1,21 @@ +package api + +import ( + "net/http" + + "os" +) + +// Env godoc +// @Summary Environment +// @Description returns the environment variables as a JSON array +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /env [get] +// @Success 200 {object} api.ArrayResponse +func (s *Server) envHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "envHandler") + defer span.End() + s.JSONResponse(w, r, os.Environ()) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/env_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/env_test.go new file mode 100644 index 0000000..0d53ce6 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/env_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +func TestEnvHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/api/env", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.infoHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := ".*hostname.*" + r := regexp.MustCompile(expected) + if !r.MatchString(rr.Body.String()) { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/headers.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/headers.go new file mode 100644 index 0000000..2cc995a --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/headers.go @@ -0,0 +1,19 @@ +package api + +import ( + "net/http" +) + +// Headers godoc +// @Summary Headers +// @Description returns a JSON array with the request HTTP headers +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /headers [get] +// @Success 200 {object} api.ArrayResponse +func (s *Server) echoHeadersHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "echoHeadersHandler") + defer span.End() + s.JSONResponse(w, r, r.Header) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/headers_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/headers_test.go new file mode 100644 index 0000000..8a7b37f --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/headers_test.go @@ -0,0 +1,36 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +func TestEchoHeadersHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/api/headers", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-Test", "testing") + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.echoHeadersHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := "testing" + r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected)) + if !r.MatchString(rr.Body.String()) { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/health.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/health.go new file mode 100644 index 0000000..61742ec --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/health.go @@ -0,0 +1,64 @@ +package api + +import ( + "net/http" + "sync/atomic" +) + +// Healthz godoc +// @Summary Liveness check +// @Description used by Kubernetes liveness probe +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Router /healthz [get] +// @Success 200 {string} string "OK" +func (s *Server) healthzHandler(w http.ResponseWriter, r *http.Request) { + if atomic.LoadInt32(&healthy) == 1 { + s.JSONResponse(w, r, map[string]string{"status": "OK"}) + return + } + w.WriteHeader(http.StatusServiceUnavailable) +} + +// Readyz godoc +// @Summary Readiness check +// @Description used by Kubernetes readiness probe +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Router /readyz [get] +// @Success 200 {string} string "OK" +func (s *Server) readyzHandler(w http.ResponseWriter, r *http.Request) { + if atomic.LoadInt32(&ready) == 1 { + s.JSONResponse(w, r, map[string]string{"status": "OK"}) + return + } + w.WriteHeader(http.StatusServiceUnavailable) +} + +// EnableReady godoc +// @Summary Enable ready state +// @Description signals the Kubernetes LB that this instance is ready to receive traffic +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Router /readyz/enable [post] +// @Success 202 {string} string "OK" +func (s *Server) enableReadyHandler(w http.ResponseWriter, r *http.Request) { + atomic.StoreInt32(&ready, 1) + w.WriteHeader(http.StatusAccepted) +} + +// DisableReady godoc +// @Summary Disable ready state +// @Description signals the Kubernetes LB to stop sending requests to this instance +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Router /readyz/disable [post] +// @Success 202 {string} string "OK" +func (s *Server) disableReadyHandler(w http.ResponseWriter, r *http.Request) { + atomic.StoreInt32(&ready, 0) + w.WriteHeader(http.StatusAccepted) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/health_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/health_test.go new file mode 100644 index 0000000..646ff05 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/health_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthzHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/healthz", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.healthzHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusServiceUnavailable { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusServiceUnavailable) + } +} + +func TestReadyzHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/readyz", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.readyzHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusServiceUnavailable { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusServiceUnavailable) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/http.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/http.go new file mode 100644 index 0000000..0061bb7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/http.go @@ -0,0 +1,94 @@ +package api + +import ( + "bytes" + "encoding/json" + "math/rand" + "net/http" + "time" + + "github.com/stefanprodan/podinfo/pkg/version" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +func randomErrorMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rand.Seed(time.Now().Unix()) + if rand.Int31n(3) == 0 { + + errors := []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusConflict} + w.WriteHeader(errors[rand.Intn(len(errors))]) + return + } + next.ServeHTTP(w, r) + }) +} + +func versionMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Header.Set("X-API-Version", version.VERSION) + r.Header.Set("X-API-Revision", version.REVISION) + + next.ServeHTTP(w, r) + }) +} + +func (s *Server) JSONResponse(w http.ResponseWriter, r *http.Request, result interface{}) { + body, err := json.Marshal(result) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + s.logger.Error("JSON marshal failed", zap.Error(err)) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusOK) + w.Write(prettyJSON(body)) +} + +func (s *Server) JSONResponseCode(w http.ResponseWriter, r *http.Request, result interface{}, responseCode int) { + body, err := json.Marshal(result) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + s.logger.Error("JSON marshal failed", zap.Error(err)) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(responseCode) + w.Write(prettyJSON(body)) +} + +func (s *Server) ErrorResponse(w http.ResponseWriter, r *http.Request, span trace.Span, error string, code int) { + data := struct { + Code int `json:"code"` + Message string `json:"message"` + }{ + Code: code, + Message: error, + } + + span.SetStatus(codes.Error, error) + + body, err := json.Marshal(data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + s.logger.Error("JSON marshal failed", zap.Error(err)) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusOK) + w.Write(prettyJSON(body)) +} + +func prettyJSON(b []byte) []byte { + var out bytes.Buffer + json.Indent(&out, b, "", " ") + return out.Bytes() +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/index.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/index.go new file mode 100644 index 0000000..a501dea --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/index.go @@ -0,0 +1,38 @@ +package api + +import ( + "html/template" + "net/http" + "path" +) + +// Index godoc +// @Summary Index +// @Description renders podinfo UI +// @Tags HTTP API +// @Produce html +// @Router / [get] +// @Success 200 {string} string "OK" +func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "indexHandler") + defer span.End() + + tmpl, err := template.New("vue.html").ParseFiles(path.Join(s.config.UIPath, "vue.html")) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(path.Join(s.config.UIPath, "vue.html") + err.Error())) + return + } + + data := struct { + Title string + Logo string + }{ + Title: s.config.Hostname, + Logo: s.config.UILogo, + } + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, path.Join(s.config.UIPath, "vue.html")+err.Error(), http.StatusInternalServerError) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/info.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/info.go new file mode 100644 index 0000000..2cd4f47 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/info.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + + "runtime" + "strconv" + + "github.com/stefanprodan/podinfo/pkg/version" +) + +// Info godoc +// @Summary Runtime information +// @Description returns the runtime information +// @Tags HTTP API +// @Accept json +// @Produce json +// @Success 200 {object} api.RuntimeResponse +// @Router /api/info [get] +func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "infoHandler") + defer span.End() + + data := RuntimeResponse{ + Hostname: s.config.Hostname, + Version: version.VERSION, + Revision: version.REVISION, + Logo: s.config.UILogo, + Color: s.config.UIColor, + Message: s.config.UIMessage, + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + Runtime: runtime.Version(), + NumGoroutine: strconv.FormatInt(int64(runtime.NumGoroutine()), 10), + NumCPU: strconv.FormatInt(int64(runtime.NumCPU()), 10), + } + + s.JSONResponse(w, r, data) +} + +type RuntimeResponse struct { + Hostname string `json:"hostname"` + Version string `json:"version"` + Revision string `json:"revision"` + Color string `json:"color"` + Logo string `json:"logo"` + Message string `json:"message"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + Runtime string `json:"runtime"` + NumGoroutine string `json:"num_goroutine"` + NumCPU string `json:"num_cpu"` +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/info_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/info_test.go new file mode 100644 index 0000000..c076be9 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/info_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +func TestInfoHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/api/info", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.infoHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := ".*color.*blue.*" + r := regexp.MustCompile(expected) + if !r.MatchString(rr.Body.String()) { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/logging.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/logging.go new file mode 100644 index 0000000..47e5c2b --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/logging.go @@ -0,0 +1,31 @@ +package api + +import ( + "net/http" + + "go.uber.org/zap" +) + +type LoggingMiddleware struct { + logger *zap.Logger +} + +func NewLoggingMiddleware(logger *zap.Logger) *LoggingMiddleware { + return &LoggingMiddleware{ + logger: logger, + } +} + +func (m *LoggingMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m.logger.Debug( + "request started", + zap.String("proto", r.Proto), + zap.String("uri", r.RequestURI), + zap.String("method", r.Method), + zap.String("remote", r.RemoteAddr), + zap.String("user-agent", r.UserAgent()), + ) + next.ServeHTTP(w, r) + }) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/metrics.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/metrics.go new file mode 100644 index 0000000..c45c86f --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/metrics.go @@ -0,0 +1,379 @@ +package api + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" +) + +type PrometheusMiddleware struct { + Histogram *prometheus.HistogramVec + Counter *prometheus.CounterVec +} + +func NewPrometheusMiddleware() *PrometheusMiddleware { + // used for monitoring and alerting (RED method) + histogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Subsystem: "http", + Name: "request_duration_seconds", + Help: "Seconds spent serving HTTP requests.", + Buckets: prometheus.DefBuckets, + }, []string{"method", "path", "status"}) + // used for horizontal pod auto-scaling (Kubernetes HPA v2) + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: "http", + Name: "requests_total", + Help: "The total number of HTTP requests.", + }, + []string{"status"}, + ) + + prometheus.MustRegister(histogram) + prometheus.MustRegister(counter) + + return &PrometheusMiddleware{ + Histogram: histogram, + Counter: counter, + } +} + +// Metrics godoc +// @Summary Prometheus metrics +// @Description returns HTTP requests duration and Go runtime metrics +// @Tags Kubernetes +// @Produce plain +// @Router /metrics [get] +// @Success 200 {string} string "OK" +func (p *PrometheusMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + begin := time.Now() + interceptor := &interceptor{ResponseWriter: w, statusCode: http.StatusOK} + path := p.getRouteName(r) + next.ServeHTTP(interceptor.wrappedResponseWriter(), r) + var ( + status = strconv.Itoa(interceptor.statusCode) + took = time.Since(begin) + ) + p.Histogram.WithLabelValues(r.Method, path, status).Observe(took.Seconds()) + p.Counter.WithLabelValues(status).Inc() + }) +} + +// converts gorilla mux routes from '/api/delay/{wait}' to 'api_delay_wait' +func (p *PrometheusMiddleware) getRouteName(r *http.Request) string { + if mux.CurrentRoute(r) != nil { + if name := mux.CurrentRoute(r).GetName(); len(name) > 0 { + return urlToLabel(name) + } + if path, err := mux.CurrentRoute(r).GetPathTemplate(); err == nil { + if len(path) > 0 { + return urlToLabel(path) + } + } + } + return urlToLabel(r.RequestURI) +} + +var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`) + +// converts a URL path to a string compatible with Prometheus label value. +func urlToLabel(path string) string { + result := invalidChars.ReplaceAllString(path, "_") + result = strings.ToLower(strings.Trim(result, "_")) + if result == "" { + result = "root" + } + return result +} + +type interceptor struct { + http.ResponseWriter + statusCode int + recorded bool +} + +func (i *interceptor) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := i.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, fmt.Errorf("interceptor: can't cast parent ResponseWriter to Hijacker") + } + return hj.Hijack() +} + +func (i *interceptor) WriteHeader(code int) { + if !i.recorded { + i.statusCode = code + i.recorded = true + } + i.ResponseWriter.WriteHeader(code) +} + +// Returns a wrapped http.ResponseWriter that implements the same optional interfaces +// that the underlying ResponseWriter has. +// Handle every possible combination so that code that checks for the existence of each +// optional interface functions properly. +// Based on https://github.com/felixge/httpsnoop/blob/eadd4fad6aac69ae62379194fe0219f3dbc80fd3/wrap_generated_gteq_1.8.go#L66 +func (i *interceptor) wrappedResponseWriter() http.ResponseWriter { + closeNotifier, isCloseNotifier := i.ResponseWriter.(http.CloseNotifier) + flush, isFlusher := i.ResponseWriter.(http.Flusher) + hijack, isHijacker := i.ResponseWriter.(http.Hijacker) + push, isPusher := i.ResponseWriter.(http.Pusher) + readFrom, isReaderFrom := i.ResponseWriter.(io.ReaderFrom) + + switch { + case !isCloseNotifier && !isFlusher && !isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + }{i} + + case isCloseNotifier && !isFlusher && !isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + }{i, closeNotifier} + + case !isCloseNotifier && isFlusher && !isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + }{i, flush} + + case !isCloseNotifier && !isFlusher && isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Hijacker + }{i, hijack} + + case !isCloseNotifier && !isFlusher && !isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Pusher + }{i, push} + + case !isCloseNotifier && !isFlusher && !isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + io.ReaderFrom + }{i, readFrom} + + case isCloseNotifier && isFlusher && !isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + }{i, closeNotifier, flush} + + case isCloseNotifier && !isFlusher && isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Hijacker + }{i, closeNotifier, hijack} + + case isCloseNotifier && !isFlusher && !isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Pusher + }{i, closeNotifier, push} + + case isCloseNotifier && !isFlusher && !isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + io.ReaderFrom + }{i, closeNotifier, readFrom} + + case !isCloseNotifier && isFlusher && isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + http.Hijacker + }{i, flush, hijack} + + case !isCloseNotifier && isFlusher && !isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + http.Pusher + }{i, flush, push} + + case !isCloseNotifier && isFlusher && !isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + io.ReaderFrom + }{i, flush, readFrom} + + case !isCloseNotifier && !isFlusher && isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + }{i, hijack, push} + + case !isCloseNotifier && !isFlusher && isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Hijacker + io.ReaderFrom + }{i, hijack, readFrom} + + case !isCloseNotifier && !isFlusher && !isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Pusher + io.ReaderFrom + }{i, push, readFrom} + + case isCloseNotifier && isFlusher && isHijacker && !isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + http.Hijacker + }{i, closeNotifier, flush, hijack} + + case isCloseNotifier && isFlusher && !isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + http.Pusher + }{i, closeNotifier, flush, push} + + case isCloseNotifier && isFlusher && !isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + io.ReaderFrom + }{i, closeNotifier, flush, readFrom} + + case isCloseNotifier && !isFlusher && isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Hijacker + http.Pusher + }{i, closeNotifier, hijack, push} + + case isCloseNotifier && !isFlusher && isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Hijacker + io.ReaderFrom + }{i, closeNotifier, hijack, readFrom} + + case isCloseNotifier && !isFlusher && !isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Pusher + io.ReaderFrom + }{i, closeNotifier, push, readFrom} + + case !isCloseNotifier && isFlusher && isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + http.Hijacker + http.Pusher + }{i, flush, hijack, push} + + case !isCloseNotifier && isFlusher && isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + http.Hijacker + io.ReaderFrom + }{i, flush, hijack, readFrom} + + case !isCloseNotifier && isFlusher && !isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + http.Pusher + io.ReaderFrom + }{i, flush, push, readFrom} + + case !isCloseNotifier && !isFlusher && isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + io.ReaderFrom + }{i, hijack, push, readFrom} + + case isCloseNotifier && isFlusher && isHijacker && isPusher && !isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + http.Hijacker + http.Pusher + }{i, closeNotifier, flush, hijack, push} + + case isCloseNotifier && isFlusher && isHijacker && !isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + http.Hijacker + io.ReaderFrom + }{i, closeNotifier, flush, hijack, readFrom} + + case isCloseNotifier && isFlusher && !isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + http.Pusher + io.ReaderFrom + }{i, closeNotifier, flush, push, readFrom} + + case isCloseNotifier && !isFlusher && isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Hijacker + http.Pusher + io.ReaderFrom + }{i, closeNotifier, hijack, push, readFrom} + + case !isCloseNotifier && isFlusher && isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.Flusher + http.Hijacker + http.Pusher + io.ReaderFrom + }{i, flush, hijack, push, readFrom} + + case isCloseNotifier && isFlusher && isHijacker && isPusher && isReaderFrom: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + http.Hijacker + http.Pusher + io.ReaderFrom + }{i, closeNotifier, flush, hijack, push, readFrom} + + default: + return struct { + http.ResponseWriter + }{i} + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/mock.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/mock.go new file mode 100644 index 0000000..7562919 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/mock.go @@ -0,0 +1,35 @@ +package api + +import ( + "time" + + "go.opentelemetry.io/otel/trace" + + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +func NewMockServer() *Server { + config := &Config{ + Port: "9898", + ServerShutdownTimeout: 5 * time.Second, + HttpServerTimeout: 30 * time.Second, + BackendURL: []string{}, + ConfigPath: "/config", + DataPath: "/data", + HttpClientTimeout: 30 * time.Second, + UIColor: "blue", + UIPath: ".ui", + UIMessage: "Greetings", + Hostname: "localhost", + } + + logger, _ := zap.NewDevelopment() + + return &Server{ + router: mux.NewRouter(), + logger: logger, + config: config, + tracer: trace.NewNoopTracerProvider().Tracer("mock"), + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/panic.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/panic.go new file mode 100644 index 0000000..3ca5fc3 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/panic.go @@ -0,0 +1,16 @@ +package api + +import ( + "net/http" + "os" +) + +// Panic godoc +// @Summary Panic +// @Description crashes the process with exit code 255 +// @Tags HTTP API +// @Router /panic [get] +func (s *Server) panicHandler(w http.ResponseWriter, r *http.Request) { + s.logger.Info("Panic command received") + os.Exit(255) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/server.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/server.go new file mode 100644 index 0000000..7075a96 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/server.go @@ -0,0 +1,311 @@ +package api + +import ( + "context" + "fmt" + "net/http" + _ "net/http/pprof" + "os" + "path" + "strings" + "sync/atomic" + "time" + + "github.com/gomodule/redigo/redis" + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus/promhttp" + _ "github.com/stefanprodan/podinfo/pkg/api/docs" + "github.com/stefanprodan/podinfo/pkg/fscache" + httpSwagger "github.com/swaggo/http-swagger" + "github.com/swaggo/swag" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +// @title Podinfo API +// @version 2.0 +// @description Go microservice template for Kubernetes. + +// @contact.name Source Code +// @contact.url https://github.com/stefanprodan/podinfo + +// @license.name MIT License +// @license.url https://github.com/stefanprodan/podinfo/blob/master/LICENSE + +// @host localhost:9898 +// @BasePath / +// @schemes http https + +var ( + healthy int32 + ready int32 + watcher *fscache.Watcher +) + +type Config struct { + HttpClientTimeout time.Duration `mapstructure:"http-client-timeout"` + HttpServerTimeout time.Duration `mapstructure:"http-server-timeout"` + ServerShutdownTimeout time.Duration `mapstructure:"server-shutdown-timeout"` + BackendURL []string `mapstructure:"backend-url"` + UILogo string `mapstructure:"ui-logo"` + UIMessage string `mapstructure:"ui-message"` + UIColor string `mapstructure:"ui-color"` + UIPath string `mapstructure:"ui-path"` + DataPath string `mapstructure:"data-path"` + ConfigPath string `mapstructure:"config-path"` + CertPath string `mapstructure:"cert-path"` + Host string `mapstructure:"host"` + Port string `mapstructure:"port"` + SecurePort string `mapstructure:"secure-port"` + PortMetrics int `mapstructure:"port-metrics"` + Hostname string `mapstructure:"hostname"` + H2C bool `mapstructure:"h2c"` + RandomDelay bool `mapstructure:"random-delay"` + RandomDelayUnit string `mapstructure:"random-delay-unit"` + RandomDelayMin int `mapstructure:"random-delay-min"` + RandomDelayMax int `mapstructure:"random-delay-max"` + RandomError bool `mapstructure:"random-error"` + Unhealthy bool `mapstructure:"unhealthy"` + Unready bool `mapstructure:"unready"` + JWTSecret string `mapstructure:"jwt-secret"` + CacheServer string `mapstructure:"cache-server"` +} + +type Server struct { + router *mux.Router + logger *zap.Logger + config *Config + pool *redis.Pool + handler http.Handler + tracer trace.Tracer + tracerProvider *sdktrace.TracerProvider +} + +func NewServer(config *Config, logger *zap.Logger) (*Server, error) { + srv := &Server{ + router: mux.NewRouter(), + logger: logger, + config: config, + } + + return srv, nil +} + +func (s *Server) registerHandlers() { + s.router.Handle("/metrics", promhttp.Handler()) + s.router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) + s.router.HandleFunc("/", s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET") + s.router.HandleFunc("/", s.infoHandler).Methods("GET") + s.router.HandleFunc("/version", s.versionHandler).Methods("GET") + s.router.HandleFunc("/echo", s.echoHandler).Methods("POST") + s.router.HandleFunc("/env", s.envHandler).Methods("GET", "POST") + s.router.HandleFunc("/headers", s.echoHeadersHandler).Methods("GET", "POST") + s.router.HandleFunc("/delay/{wait:[0-9]+}", s.delayHandler).Methods("GET").Name("delay") + s.router.HandleFunc("/healthz", s.healthzHandler).Methods("GET") + s.router.HandleFunc("/readyz", s.readyzHandler).Methods("GET") + s.router.HandleFunc("/readyz/enable", s.enableReadyHandler).Methods("POST") + s.router.HandleFunc("/readyz/disable", s.disableReadyHandler).Methods("POST") + s.router.HandleFunc("/panic", s.panicHandler).Methods("GET") + s.router.HandleFunc("/status/{code:[0-9]+}", s.statusHandler).Methods("GET", "POST", "PUT").Name("status") + s.router.HandleFunc("/store", s.storeWriteHandler).Methods("POST", "PUT") + s.router.HandleFunc("/store/{hash}", s.storeReadHandler).Methods("GET").Name("store") + s.router.HandleFunc("/cache/{key}", s.cacheWriteHandler).Methods("POST", "PUT") + s.router.HandleFunc("/cache/{key}", s.cacheDeleteHandler).Methods("DELETE") + s.router.HandleFunc("/cache/{key}", s.cacheReadHandler).Methods("GET").Name("cache") + s.router.HandleFunc("/configs", s.configReadHandler).Methods("GET") + s.router.HandleFunc("/token", s.tokenGenerateHandler).Methods("POST") + s.router.HandleFunc("/token/validate", s.tokenValidateHandler).Methods("GET") + s.router.HandleFunc("/api/info", s.infoHandler).Methods("GET") + s.router.HandleFunc("/api/echo", s.echoHandler).Methods("POST") + s.router.HandleFunc("/ws/echo", s.echoWsHandler) + s.router.HandleFunc("/chunked", s.chunkedHandler) + s.router.HandleFunc("/chunked/{wait:[0-9]+}", s.chunkedHandler) + s.router.PathPrefix("/swagger/").Handler(httpSwagger.Handler( + httpSwagger.URL("/swagger/doc.json"), + )) + s.router.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) { + doc, err := swag.ReadDoc() + if err != nil { + s.logger.Error("swagger error", zap.Error(err), zap.String("path", "/swagger.json")) + } + w.Write([]byte(doc)) + }) +} + +func (s *Server) registerMiddlewares() { + prom := NewPrometheusMiddleware() + s.router.Use(prom.Handler) + otel := NewOpenTelemetryMiddleware() + s.router.Use(otel) + httpLogger := NewLoggingMiddleware(s.logger) + s.router.Use(httpLogger.Handler) + s.router.Use(versionMiddleware) + if s.config.RandomDelay { + randomDelayer := NewRandomDelayMiddleware(s.config.RandomDelayMin, s.config.RandomDelayMax, s.config.RandomDelayUnit) + s.router.Use(randomDelayer.Handler) + } + if s.config.RandomError { + s.router.Use(randomErrorMiddleware) + } +} + +func (s *Server) ListenAndServe() (*http.Server, *http.Server, *int32, *int32) { + ctx := context.Background() + + go s.startMetricsServer() + + s.initTracer(ctx) + s.registerHandlers() + s.registerMiddlewares() + + if s.config.H2C { + s.handler = h2c.NewHandler(s.router, &http2.Server{}) + } else { + s.handler = s.router + } + + //s.printRoutes() + + // load configs in memory and start watching for changes in the config dir + if stat, err := os.Stat(s.config.ConfigPath); err == nil && stat.IsDir() { + var err error + watcher, err = fscache.NewWatch(s.config.ConfigPath) + if err != nil { + s.logger.Error("config watch error", zap.Error(err), zap.String("path", s.config.ConfigPath)) + } else { + watcher.Watch() + } + } + + // start redis connection pool + ticker := time.NewTicker(30 * time.Second) + s.startCachePool(ticker) + + // create the http server + srv := s.startServer() + + // create the secure server + secureSrv := s.startSecureServer() + + // signal Kubernetes the server is ready to receive traffic + if !s.config.Unhealthy { + atomic.StoreInt32(&healthy, 1) + } + if !s.config.Unready { + atomic.StoreInt32(&ready, 1) + } + + return srv, secureSrv, &healthy, &ready +} + +func (s *Server) startServer() *http.Server { + + // determine if the port is specified + if s.config.Port == "0" { + + // move on immediately + return nil + } + + srv := &http.Server{ + Addr: s.config.Host + ":" + s.config.Port, + WriteTimeout: s.config.HttpServerTimeout, + ReadTimeout: s.config.HttpServerTimeout, + IdleTimeout: 2 * s.config.HttpServerTimeout, + Handler: s.handler, + } + + // start the server in the background + go func() { + s.logger.Info("Starting HTTP Server.", zap.String("addr", srv.Addr)) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + s.logger.Fatal("HTTP server crashed", zap.Error(err)) + } + }() + + // return the server and routine + return srv +} + +func (s *Server) startSecureServer() *http.Server { + + // determine if the port is specified + if s.config.SecurePort == "0" { + + // move on immediately + return nil + } + + srv := &http.Server{ + Addr: s.config.Host + ":" + s.config.SecurePort, + WriteTimeout: s.config.HttpServerTimeout, + ReadTimeout: s.config.HttpServerTimeout, + IdleTimeout: 2 * s.config.HttpServerTimeout, + Handler: s.handler, + } + + cert := path.Join(s.config.CertPath, "tls.crt") + key := path.Join(s.config.CertPath, "tls.key") + + // start the server in the background + go func() { + s.logger.Info("Starting HTTPS Server.", zap.String("addr", srv.Addr)) + if err := srv.ListenAndServeTLS(cert, key); err != http.ErrServerClosed { + s.logger.Fatal("HTTPS server crashed", zap.Error(err)) + } + }() + + // return the server + return srv +} + +func (s *Server) startMetricsServer() { + if s.config.PortMetrics > 0 { + mux := http.DefaultServeMux + mux.Handle("/metrics", promhttp.Handler()) + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%v", s.config.PortMetrics), + Handler: mux, + } + + srv.ListenAndServe() + } +} + +func (s *Server) printRoutes() { + s.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) +} + +type ArrayResponse []string +type MapResponse map[string]string diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/status.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/status.go new file mode 100644 index 0000000..e9e33ae --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/status.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + + "strconv" + + "github.com/gorilla/mux" +) + +// Status godoc +// @Summary Status code +// @Description sets the response status code to the specified code +// @Tags HTTP API +// @Accept json +// @Produce json +// @Param code path int true "status code to return" +// @Router /status/{code} [get] +// @Success 200 {object} api.MapResponse +func (s *Server) statusHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "statusHandler") + defer span.End() + + vars := mux.Vars(r) + + code, err := strconv.Atoi(vars["code"]) + if err != nil { + s.ErrorResponse(w, r, span, err.Error(), http.StatusBadRequest) + return + } + + s.JSONResponseCode(w, r, map[string]int{"status": code}, code) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/status_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/status_test.go new file mode 100644 index 0000000..f85f2b2 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/status_test.go @@ -0,0 +1,26 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestStatusHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/status/404", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + + srv.router.HandleFunc("/status/{code}", srv.statusHandler) + srv.router.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusNotFound { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusNotFound) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/store.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/store.go new file mode 100644 index 0000000..a97db5e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/store.go @@ -0,0 +1,71 @@ +package api + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "net/http" + "os" + "path" + + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +// Store godoc +// @Summary Upload file +// @Description writes the posted content to disk at /data/hash and returns the SHA1 hash of the content +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /store [post] +// @Success 200 {object} api.MapResponse +func (s *Server) storeWriteHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "storeWriteHandler") + defer span.End() + + body, err := io.ReadAll(r.Body) + if err != nil { + s.ErrorResponse(w, r, span, "reading the request body failed", http.StatusBadRequest) + return + } + + hash := hash(string(body)) + err = os.WriteFile(path.Join(s.config.DataPath, hash), body, 0644) + if err != nil { + s.logger.Warn("writing file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash))) + s.ErrorResponse(w, r, span, "writing file failed", http.StatusInternalServerError) + return + } + s.JSONResponseCode(w, r, map[string]string{"hash": hash}, http.StatusAccepted) +} + +// Store godoc +// @Summary Download file +// @Description returns the content of the file /data/hash if exists +// @Tags HTTP API +// @Accept json +// @Produce plain +// @Param hash path string true "hash value" +// @Router /store/{hash} [get] +// @Success 200 {string} string "file" +func (s *Server) storeReadHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "storeReadHandler") + defer span.End() + + hash := mux.Vars(r)["hash"] + content, err := os.ReadFile(path.Join(s.config.DataPath, hash)) + if err != nil { + s.logger.Warn("reading file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash))) + s.ErrorResponse(w, r, span, "reading file failed", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(content)) +} + +func hash(input string) string { + h := sha1.New() + h.Write([]byte(input)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/token.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/token.go new file mode 100644 index 0000000..c391860 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/token.go @@ -0,0 +1,129 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + "go.uber.org/zap" +) + +type jwtCustomClaims struct { + Name string `json:"name"` + jwt.StandardClaims +} + +// Token godoc +// @Summary Generate JWT token +// @Description issues a JWT token valid for one minute +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /token [post] +// @Success 200 {object} api.TokenResponse +func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "tokenGenerateHandler") + defer span.End() + + body, err := io.ReadAll(r.Body) + if err != nil { + s.logger.Error("reading the request body failed", zap.Error(err)) + s.ErrorResponse(w, r, span, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + user := "anonymous" + if len(body) > 0 { + user = string(body) + } + + expiresAt := time.Now().Add(time.Minute * 1) + claims := &jwtCustomClaims{ + user, + jwt.StandardClaims{ + Issuer: "podinfo", + ExpiresAt: expiresAt.Unix(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + t, err := token.SignedString([]byte(s.config.JWTSecret)) + if err != nil { + s.ErrorResponse(w, r, span, err.Error(), http.StatusBadRequest) + return + } + + var result = TokenResponse{ + Token: t, + ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0), + } + + s.JSONResponse(w, r, result) +} + +// TokenValidate godoc +// @Summary Validate JWT token +// @Description validates the JWT token +// @Tags HTTP API +// @Accept json +// @Produce json +// @Router /token/validate [post] +// @Success 200 {object} api.TokenValidationResponse +// @Failure 401 {string} string "Unauthorized" +// Get: JWT=$(curl -s -d 'test' localhost:9898/token | jq -r .token) +// Post: curl -H "Authorization: Bearer ${JWT}" localhost:9898/token/validate +func (s *Server) tokenValidateHandler(w http.ResponseWriter, r *http.Request) { + _, span := s.tracer.Start(r.Context(), "tokenValidateHandler") + defer span.End() + + authorizationHeader := r.Header.Get("authorization") + if authorizationHeader == "" { + s.ErrorResponse(w, r, span, "authorization bearer header required", http.StatusUnauthorized) + return + } + bearerToken := strings.Split(authorizationHeader, " ") + if len(bearerToken) != 2 || strings.ToLower(bearerToken[0]) != "bearer" { + s.ErrorResponse(w, r, span, "authorization bearer header required", http.StatusUnauthorized) + return + } + + claims := jwtCustomClaims{} + token, err := jwt.ParseWithClaims(bearerToken[1], &claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("invalid signing method") + } + return []byte(s.config.JWTSecret), nil + }) + if err != nil { + s.ErrorResponse(w, r, span, err.Error(), http.StatusUnauthorized) + return + } + + if token.Valid { + if claims.StandardClaims.Issuer != "podinfo" { + s.ErrorResponse(w, r, span, "invalid issuer", http.StatusUnauthorized) + } else { + var result = TokenValidationResponse{ + TokenName: claims.Name, + ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0), + } + s.JSONResponse(w, r, result) + } + } else { + s.ErrorResponse(w, r, span, "Invalid authorization token", http.StatusUnauthorized) + } +} + +type TokenResponse struct { + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` +} + +type TokenValidationResponse struct { + TokenName string `json:"token_name"` + ExpiresAt time.Time `json:"expires_at"` +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/token_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/token_test.go new file mode 100644 index 0000000..a82824b --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/token_test.go @@ -0,0 +1,36 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestTokenHandler(t *testing.T) { + req, err := http.NewRequest("POST", "/token", strings.NewReader("test-user")) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.tokenGenerateHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + var token TokenResponse + if err := json.Unmarshal(rr.Body.Bytes(), &token); err != nil { + t.Fatal(err) + } + if token.Token == "" { + t.Error("handler returned no token") + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/tracer.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/tracer.go new file mode 100644 index 0000000..ec7c208 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/tracer.go @@ -0,0 +1,70 @@ +package api + +import ( + "context" + + "github.com/gorilla/mux" + "github.com/spf13/viper" + "github.com/stefanprodan/podinfo/pkg/version" + "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" + "go.opentelemetry.io/contrib/propagators/aws/xray" + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/contrib/propagators/ot" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +const ( + instrumentationName = "github.com/stefanprodan/podinfo/pkg/api" +) + +func (s *Server) initTracer(ctx context.Context) { + if viper.GetString("otel-service-name") == "" { + nop := trace.NewNoopTracerProvider() + s.tracer = nop.Tracer(viper.GetString("otel-service-name")) + return + } + + client := otlptracegrpc.NewClient() + exporter, err := otlptrace.New(ctx, client) + if err != nil { + s.logger.Error("creating OTLP trace exporter", zap.Error(err)) + } + + s.tracerProvider = sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(viper.GetString("otel-service-name")), + semconv.ServiceVersionKey.String(version.VERSION), + )), + ) + + otel.SetTracerProvider(s.tracerProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + b3.New(), + &jaeger.Jaeger{}, + &ot.OT{}, + &xray.Propagator{}, + )) + + s.tracer = s.tracerProvider.Tracer( + instrumentationName, + trace.WithInstrumentationVersion(version.VERSION), + trace.WithSchemaURL(semconv.SchemaURL), + ) +} + +func NewOpenTelemetryMiddleware() mux.MiddlewareFunc { + return otelmux.Middleware(viper.GetString("otel-service-name")) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/version.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/version.go new file mode 100644 index 0000000..037691e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/version.go @@ -0,0 +1,22 @@ +package api + +import ( + "net/http" + + "github.com/stefanprodan/podinfo/pkg/version" +) + +// Version godoc +// @Summary Version +// @Description returns podinfo version and git commit hash +// @Tags HTTP API +// @Produce json +// @Router /version [get] +// @Success 200 {object} api.MapResponse +func (s *Server) versionHandler(w http.ResponseWriter, r *http.Request) { + result := map[string]string{ + "version": version.VERSION, + "commit": version.REVISION, + } + s.JSONResponse(w, r, result) +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/api/version_test.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/version_test.go new file mode 100644 index 0000000..e7ee256 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/api/version_test.go @@ -0,0 +1,36 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "regexp" + "testing" +) + +func TestVersionHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/version", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + srv := NewMockServer() + handler := http.HandlerFunc(srv.versionHandler) + + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := "unknown" + r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected)) + if !r.MatchString(rr.Body.String()) { + t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", + rr.Body.String(), expected) + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/fscache/fscache.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/fscache/fscache.go new file mode 100644 index 0000000..9c9a977 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/fscache/fscache.go @@ -0,0 +1,112 @@ +package fscache + +import ( + "errors" + "log" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/fsnotify/fsnotify" +) + +type Watcher struct { + dir string + fswatcher *fsnotify.Watcher + Cache *sync.Map +} + +// NewWatch creates a directory watcher and +// updates the cache when any file changes in that dir +func NewWatch(dir string) (*Watcher, error) { + if len(dir) < 1 { + return nil, errors.New("directory is empty") + } + + fw, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + w := &Watcher{ + dir: dir, + fswatcher: fw, + Cache: new(sync.Map), + } + + log.Printf("fscache start watcher for %s", w.dir) + err = w.fswatcher.Add(w.dir) + if err != nil { + return nil, err + } + + // initial read + err = w.updateCache() + if err != nil { + return nil, err + } + + return w, nil +} + +// Watch watches for when kubelet updates the volume mount content +func (w *Watcher) Watch() { + go func() { + for { + select { + // it can take up to a 2 minutes for kubelet to recreate the ..data symlink + case event := <-w.fswatcher.Events: + if event.Op&fsnotify.Create == fsnotify.Create { + if filepath.Base(event.Name) == "..data" { + err := w.updateCache() + if err != nil { + log.Printf("fscache update error %v", err) + } else { + log.Printf("fscache reload %s", w.dir) + } + } + } + case err := <-w.fswatcher.Errors: + log.Printf("fswatcher %s error %v", w.dir, err) + } + } + }() +} + +// updateCache reads files content and loads them into the cache +func (w *Watcher) updateCache() error { + fileMap := make(map[string]string) + files, err := os.ReadDir(w.dir) + if err != nil { + return err + } + + // read files ignoring symlinks and sub directories + for _, file := range files { + name := filepath.Base(file.Name()) + if !file.IsDir() && !strings.Contains(name, "..") { + b, err := os.ReadFile(filepath.Join(w.dir, file.Name())) + if err != nil { + return err + } + fileMap[name] = string(b) + } + } + + // remove deleted files from cache + w.Cache.Range(func(key interface{}, value interface{}) bool { + _, ok := fileMap[key.(string)] + if !ok { + w.Cache.Delete(key) + } + return true + }) + + // sync cache + for k, v := range fileMap { + w.Cache.Store(k, v) + } + + return nil +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/grpc/server.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/grpc/server.go new file mode 100644 index 0000000..3de371f --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/grpc/server.go @@ -0,0 +1,52 @@ +package grpc + +import ( + "fmt" + "net" + + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +type Server struct { + logger *zap.Logger + config *Config +} + +type Config struct { + Port int `mapstructure:"grpc-port"` + ServiceName string `mapstructure:"grpc-service-name"` +} + +func NewServer(config *Config, logger *zap.Logger) (*Server, error) { + srv := &Server{ + logger: logger, + config: config, + } + + return srv, nil +} + +func (s *Server) ListenAndServe() *grpc.Server { + listener, err := net.Listen("tcp", fmt.Sprintf(":%v", s.config.Port)) + if err != nil { + s.logger.Fatal("failed to listen", zap.Int("port", s.config.Port)) + } + + srv := grpc.NewServer() + server := health.NewServer() + reflection.Register(srv) + grpc_health_v1.RegisterHealthServer(srv, server) + server.SetServingStatus(s.config.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + + go func() { + if err := srv.Serve(listener); err != nil { + s.logger.Fatal("failed to serve", zap.Error(err)) + } + }() + + return srv +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/shutdown.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/shutdown.go new file mode 100644 index 0000000..478576e --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/shutdown.go @@ -0,0 +1,83 @@ +package signals + +import ( + "context" + "net/http" + "sync/atomic" + "time" + + "github.com/gomodule/redigo/redis" + "github.com/spf13/viper" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +type Shutdown struct { + logger *zap.Logger + pool *redis.Pool + tracerProvider *sdktrace.TracerProvider + serverShutdownTimeout time.Duration +} + +func NewShutdown(serverShutdownTimeout time.Duration, logger *zap.Logger) (*Shutdown, error) { + srv := &Shutdown{ + logger: logger, + serverShutdownTimeout: serverShutdownTimeout, + } + + return srv, nil +} + +func (s *Shutdown) Graceful(stopCh <-chan struct{}, httpServer *http.Server, httpsServer *http.Server, grpcServer *grpc.Server, healthy *int32, ready *int32) { + ctx := context.Background() + + // wait for SIGTERM or SIGINT + <-stopCh + ctx, cancel := context.WithTimeout(ctx, s.serverShutdownTimeout) + defer cancel() + + // all calls to /healthz and /readyz will fail from now on + atomic.StoreInt32(healthy, 0) + atomic.StoreInt32(ready, 0) + + // close cache pool + if s.pool != nil { + _ = s.pool.Close() + } + + s.logger.Info("Shutting down HTTP/HTTPS server", zap.Duration("timeout", s.serverShutdownTimeout)) + + // wait for Kubernetes readiness probe to remove this instance from the load balancer + // the readiness check interval must be lower than the timeout + if viper.GetString("level") != "debug" { + time.Sleep(3 * time.Second) + } + + // stop OpenTelemetry tracer provider + if s.tracerProvider != nil { + if err := s.tracerProvider.Shutdown(ctx); err != nil { + s.logger.Warn("stopping tracer provider", zap.Error(err)) + } + } + + // determine if the GRPC was started + if grpcServer != nil { + s.logger.Info("Shutting down GRPC server", zap.Duration("timeout", s.serverShutdownTimeout)) + grpcServer.GracefulStop() + } + + // determine if the http server was started + if httpServer != nil { + if err := httpServer.Shutdown(ctx); err != nil { + s.logger.Warn("HTTP server graceful shutdown failed", zap.Error(err)) + } + } + + // determine if the secure server was started + if httpsServer != nil { + if err := httpsServer.Shutdown(ctx); err != nil { + s.logger.Warn("HTTPS server graceful shutdown failed", zap.Error(err)) + } + } +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal.go new file mode 100644 index 0000000..f82aafb --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal.go @@ -0,0 +1,27 @@ +package signals + +import ( + "os" + "os/signal" +) + +var onlyOneSignalHandler = make(chan struct{}) + +// SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned +// which is closed on one of these signals. If a second signal is caught, the program +// is terminated with exit code 1. +func SetupSignalHandler() (stopCh <-chan struct{}) { + close(onlyOneSignalHandler) // panics when called twice + + stop := make(chan struct{}) + c := make(chan os.Signal, 2) + signal.Notify(c, shutdownSignals...) + go func() { + <-c + close(stop) + <-c + os.Exit(1) // second signal. Exit directly. + }() + + return stop +} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal_posix.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal_posix.go new file mode 100644 index 0000000..80d74d2 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal_posix.go @@ -0,0 +1,11 @@ +//go:build !windows +// +build !windows + +package signals + +import ( + "os" + "syscall" +) + +var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGINT} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal_windows.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal_windows.go new file mode 100644 index 0000000..f2015c1 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/signals/signal_windows.go @@ -0,0 +1,7 @@ +package signals + +import ( + "os" +) + +var shutdownSignals = []os.Signal{os.Interrupt} diff --git a/examples/gitops-kubernetes-application-pipeline/src/pkg/version/version.go b/examples/gitops-kubernetes-application-pipeline/src/pkg/version/version.go new file mode 100644 index 0000000..4957f38 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/pkg/version/version.go @@ -0,0 +1,4 @@ +package version + +var VERSION = "6.3.5" +var REVISION = "unknown" diff --git a/examples/gitops-kubernetes-application-pipeline/src/test/build.sh b/examples/gitops-kubernetes-application-pipeline/src/test/build.sh new file mode 100755 index 0000000..2bac9f4 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/test/build.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env sh + +set -e + +# build the docker file +GIT_COMMIT=$(git rev-list -1 HEAD) && \ +DOCKER_BUILDKIT=1 docker build --tag test/podinfo --build-arg "REVISION=${GIT_COMMIT}" . diff --git a/examples/gitops-kubernetes-application-pipeline/src/test/deploy.sh b/examples/gitops-kubernetes-application-pipeline/src/test/deploy.sh new file mode 100755 index 0000000..75356a4 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/test/deploy.sh @@ -0,0 +1,29 @@ +#! /usr/bin/env sh + +# install cert-manager +kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.3/cert-manager.yaml + +# wait for cert manager +kubectl -n cert-manager rollout status deployment/cert-manager --timeout=2m +kubectl -n cert-manager rollout status deployment/cert-manager-webhook --timeout=2m +kubectl -n cert-manager rollout status deployment/cert-manager-cainjector --timeout=2m + +# install self-signed certificate +cat << 'EOF' | kubectl apply -f - +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: self-signed +spec: + selfSigned: {} +EOF + +# install podinfo with tls enabled +helm upgrade --install podinfo ./charts/podinfo \ + --set image.repository=test/podinfo \ + --set image.tag=latest \ + --set tls.enabled=true \ + --set certificate.create=true \ + --set hpa.enabled=true \ + --set hpa.cpu=95 \ + --namespace=default diff --git a/examples/gitops-kubernetes-application-pipeline/src/test/e2e.sh b/examples/gitops-kubernetes-application-pipeline/src/test/e2e.sh new file mode 100755 index 0000000..586f9dd --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/test/e2e.sh @@ -0,0 +1,20 @@ +#! /usr/bin/env sh + +set -e + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +# run the build +$SCRIPT_DIR/build.sh + +# create the kind cluster +kind create cluster || true + +# load the docker image +kind load docker-image test/podinfo:latest + +# run the deploy +$SCRIPT_DIR/deploy.sh + +# run the tests +$SCRIPT_DIR/test.sh diff --git a/examples/gitops-kubernetes-application-pipeline/src/test/test.sh b/examples/gitops-kubernetes-application-pipeline/src/test/test.sh new file mode 100755 index 0000000..842fce7 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/test/test.sh @@ -0,0 +1,9 @@ +#1 /usr/bin/env sh + +set -e + +# wait for podinfo +kubectl rollout status deployment/podinfo --timeout=3m + +# test podinfo +helm test podinfo diff --git a/examples/gitops-kubernetes-application-pipeline/src/ui/vue.html b/examples/gitops-kubernetes-application-pipeline/src/ui/vue.html new file mode 100644 index 0000000..e4c26d5 --- /dev/null +++ b/examples/gitops-kubernetes-application-pipeline/src/ui/vue.html @@ -0,0 +1,165 @@ + + + + {{.Title}} + + + + + + + + + +
+ + +
+ + + +

${ info.message }

+
Served by ${ info.hostname }
+ + + ${ pings } + touch_app + + Ping + + + + +
+
+
+ + + +
+ Powered + by podinfo + version ${ info.version } revision ${ info.revision } + Swagger docs +
+
+
+
+
+
+
+ + + + + +