primera vegada

This commit is contained in:
celoman 2025-04-14 19:48:55 +00:00
parent 422621c69c
commit 7ac76c2e46
276 changed files with 15213 additions and 1 deletions

View File

@ -8,6 +8,6 @@ spec:
selector:
app: nginx
ports:
- port: 80
- port: 11111
targetPort: 80
type: LoadBalancer

View File

@ -0,0 +1 @@
https://github.com/caddyserver/ingress

View File

@ -0,0 +1,13 @@
/.github
/bin
/charts
/kubernetes
/.gitignore
/.goreleaser.yaml
/CONTRIBUTING.md
/ct.yaml
/skaffold.yaml
/README.md
/Makefile
/LICENSE.txt

View File

@ -0,0 +1,6 @@
.DS_Store
bin
vendor
.idea/
dist/
ingress-controller

View File

@ -0,0 +1,112 @@
project_name: ingress
before:
hooks:
- go mod tidy
builds:
- main: ./cmd/caddy
binary: ingress-controller
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
- s390x
mod_timestamp: "{{ .CommitTimestamp }}"
dockers:
- use: buildx
goos: linux
goarch: amd64
dockerfile: ./Dockerfile
skip_push: "true"
image_templates:
- "caddy/{{ .ProjectName }}:test-image"
build_flag_templates:
- "--platform=linux/amd64"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- use: buildx
goos: linux
goarch: amd64
dockerfile: ./Dockerfile
image_templates:
- "caddy/{{ .ProjectName }}:{{ .Tag }}-amd64"
build_flag_templates:
- "--platform=linux/amd64"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- use: buildx
goos: linux
goarch: arm64
dockerfile: ./Dockerfile
image_templates:
- "caddy/{{ .ProjectName }}:{{ .Tag }}-arm64v8"
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- use: buildx
goos: linux
goarch: s390x
dockerfile: ./Dockerfile
image_templates:
- "caddy/{{ .ProjectName }}:{{ .Tag }}-s390x"
build_flag_templates:
- "--platform=linux/s390x"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
docker_manifests:
# https://goreleaser.com/customization/docker_manifest/
- name_template: caddy/{{ .ProjectName }}:latest
image_templates:
- caddy/{{ .ProjectName }}:{{ .Tag }}-amd64
- caddy/{{ .ProjectName }}:{{ .Tag }}-arm64v8
- caddy/{{ .ProjectName }}:{{ .Tag }}-s390x
- name_template: caddy/{{ .ProjectName }}:v{{ .Major }}
image_templates:
- caddy/{{ .ProjectName }}:{{ .Tag }}-amd64
- caddy/{{ .ProjectName }}:{{ .Tag }}-arm64v8
- caddy/{{ .ProjectName }}:{{ .Tag }}-s390x
- name_template: caddy/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}
image_templates:
- caddy/{{ .ProjectName }}:{{ .Tag }}-amd64
- caddy/{{ .ProjectName }}:{{ .Tag }}-arm64v8
- caddy/{{ .ProjectName }}:{{ .Tag }}-s390x
- name_template: caddy/{{ .ProjectName }}:{{ .Tag }}
image_templates:
- caddy/{{ .ProjectName }}:{{ .Tag }}-amd64
- caddy/{{ .ProjectName }}:{{ .Tag }}-arm64v8
- caddy/{{ .ProjectName }}:{{ .Tag }}-s390x
release:
disable: true
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"

View File

@ -0,0 +1,54 @@
## Requirements
- A running kubernetes cluster (if you don't have one see *Setup a local cluster* section)
- [skaffold](https://skaffold.dev/) installed on your machine
- [helm 3](https://helm.sh/) installed on your machine
- [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl/) installed on your machine
### Setup a local cluster
- You need a machine with [docker](https://docker.io) up & running
- If you are using Docker Desktop enable the kubernetes cluster from the config (see [docs](https://docs.docker.com/desktop/kubernetes/) here)
Otherwise you can decide to use [kind]() or [minukube]()
## Setup development env
Start `skaffold` using the command:
```
make dev
```
this will automatically:
- build your docker image every time you change some code
- update the helm release every time you change the helm chart
- expose the caddy ingress controller (port 8080 and 8443)
You can test that all work as expected with:
```
curl -D - -s -k https://example1.kubernetes.localhost:8443/hello1 --resolve example1.kubernetes.localhost:8443:127.0.0.1
curl -D - -s -k https://example1.kubernetes.localhost:8443/hello2 --resolve example1.kubernetes.localhost:8443:127.0.0.1
curl -D - -s -k https://example2.kubernetes.localhost:8443/hello1 --resolve example2.kubernetes.localhost:8443:127.0.0.1
curl -D - -s -k https://example2.kubernetes.localhost:8443/hello2 --resolve example2.kubernetes.localhost:8443:127.0.0.1
```
You can change domains defined in `kuberentes/sample` folder with some domain that are risolved on your local machine or you can add them in the `/etc/host` file to be automatically resolved as localhost.
## Notes
- You can change local port forwarded by skaffold by changing the port values in the `skaffold.yaml` file on section `portForward` `localPort`. Remind that you can forward only port greater than 1024 if you execute it as non-root user
- We use an internal CA for test to simplify the installation, but then you can decide to use external CA for tests, see the `values.yaml` in the helm chart for the info on how to configure it.
## Releasing new helm chart version
If you want to release a new version of the `caddy-ingress-controller` chart, you'll need
to create a new PR with:
- The new chart's `version` in `Chart.yaml`
- The new `image.tag` in `values.yaml` (if you want to update the default image used in the chart)
- The new `appVersion` in `Chart.yaml` (if you did the previous line)
## Releasing a new app version
To release a new caddy-ingress-controller image, you need to create a new semver tag.
It will build and push an image to https://hub.docker.com/r/caddy/ingress.

View File

@ -0,0 +1,10 @@
FROM alpine:latest AS certs
RUN apk --update add ca-certificates
FROM alpine:latest
EXPOSE 80 443
ENTRYPOINT ["/ingress-controller"]
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY ingress-controller /

View File

@ -0,0 +1,19 @@
FROM golang:1.22 as build
ENV CGO_ENABLED=0
WORKDIR /go/ingress-controller
COPY go.mod go.sum /go/ingress-controller/
RUN go mod download
COPY . .
RUN go build -o ./bin/ingress-controller ./cmd/caddy
FROM alpine:3.18
EXPOSE 80 443
COPY --from=build /go/ingress-controller/bin/ingress-controller /ingress-controller
ENTRYPOINT ["/ingress-controller"]

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,6 @@
build:
@mkdir -p bin
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/ingress-controller ./cmd/caddy
dev:
skaffold dev --port-forward

View File

@ -0,0 +1,153 @@
# Caddy Ingress Controller
This is the Kubernetes Ingress Controller for Caddy. It includes functionality
for monitoring `Ingress` resources on a Kubernetes cluster and includes support
for providing automatic HTTPS certificates for all hostnames defined in the
ingress resources that it is managing.
## Prerequisites
- Helm 3+
- Kubernetes 1.19+
## Setup
In the `charts` folder, a Helm Chart is provided to make installing the Caddy
Ingress Controller on a Kubernetes cluster straightforward. To install the
Caddy Ingress Controller adhere to the following steps:
1. Create a new namespace in your cluster to isolate all Caddy resources.
```sh
kubectl create namespace caddy-system
```
2. Install the Helm Chart.
```sh
helm install \
--namespace=caddy-system \
--repo https://caddyserver.github.io/ingress/ \
--atomic \
mycaddy \
caddy-ingress-controller
```
Or
2. Generate kubernetes yaml file.
```sh
git clone https://github.com/caddyserver/ingress.git
cd ingress
# generate the yaml file
helm template mycaddy ./charts/caddy-ingress-controller \
--namespace=caddy-system \
> mycaddy.yaml
# apply the file
kubectl apply -f mycaddy.yaml
```
This will create a service of type `LoadBalancer` in the `caddy-system`
namespace on your cluster. You'll want to set any DNS records for accessing this
cluster to the external IP address of this `LoadBalancer` when the external IP
is provisioned by your cloud provider.
You can get the external IP address with `kubectl get svc -n caddy-system`
3. Alternate installation method: Glasskube
To install the Caddy ingress controller using [Glasskube](https://glasskube.dev/), you can select "caddy-ingress-controller" from the "ClusterPackages" tab in the Glasskube GUI then click "install" or you can run the following command:
```console
glasskube install caddy-ingress-controller
```
Add an email address in the package configuration section in the UI to enable automatic HTTPS, or run:
```
glasskube install caddy-ingress-controller --value "automaticHTTPS=your@email.com"
```
## Debugging
To view any logs generated by Caddy or the Ingress Controller you can view the
pod logs of the Caddy Ingress Controller.
Get the pod name with:
```sh
kubectl get pods -n caddy-system
```
View the pod logs:
```sh
kubectl logs <pod-name> -n caddy-system
```
## Automatic HTTPS
To have automatic HTTPS (not to be confused with `On-demand TLS`), you simply have
to specify your email in the config map. When using Helm chart, you can add
`--set ingressController.config.email=your@email.com` when installing.
## On-Demand TLS
[On-demand TLS](https://caddyserver.com/docs/automatic-https#on-demand-tls) can generate SSL certs on the fly
and can be enabled in this controller by setting the `onDemandTLS` config to `true`:
```sh
helm install ...\
--set ingressController.config.onDemandTLS=true
```
> You can also specify options
> for the on-demand config: `onDemandAsk`
## Bringing Your Own Certificates
If you would like to disable automatic HTTPS for a specific host and use your
own certificates you can create a new TLS secret in Kubernetes and define what
certificates to use when serving your application on the ingress resource.
Example:
Create TLS secret `mycerts`, where `./tls.key` and `./tls.crt` are valid
certificates for `test.com`.
```
kubectl create secret tls mycerts --key ./tls.key --cert ./tls.crt
```
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
annotations:
kubernetes.io/ingress.class: caddy
spec:
rules:
- host: test.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test
port:
number: 8080
tls:
- secretName: mycerts # use mycerts for host test.com
hosts:
- test.com
```
### Contribution
Learn how to start contributing on the [Contributing Guidline](CONTRIBUTING.md).
## License
[Apache License 2.0](LICENSE.txt)

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,18 @@
apiVersion: v2
name: caddy-ingress-controller
home: https://github.com/caddyserver/ingress
description: A helm chart for the Caddy Kubernetes ingress controller
icon: https://caddyserver.com/resources/images/caddy-circle-lock.svg
type: application
version: 1.3.0
appVersion: "v0.2.1"
keywords:
- ingress-controller
- caddyserver
sources:
- https://github.com/caddyserver/ingress
maintainers:
- name: mavimo
url: https://github.com/mavimo
- name: embraser01
url: https://github.com/embraser01

View File

@ -0,0 +1,84 @@
# caddy-ingress-controller
A helm chart for the Caddy Kubernetes ingress controller
## TL;DR:
```bash
helm install my-release caddy-ingress-controller\
--repo https://caddyserver.github.io/ingress/ \
--namespace=caddy-system
```
## Introduction
This chart bootstraps a caddy-ingress-deployment deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
## Prerequisites
- Helm 3+
- Kubernetes 1.19+
## Installing the Chart
```bash
helm repo add caddyserver https://caddyserver.github.io/ingress/
helm install my-release caddyserver/caddy-ingress-controller --namespace=caddy-system
```
## Uninstalling the Chart
To uninstall `my-release`:
```console
$ helm uninstall my-release
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
> **Tip**: List all releases using `helm list` or start clean with `helm uninstall my-release`
## Additional Configuration
## Troubleshooting
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"caddy/ingress"` | |
| image.tag | string | `"latest"` | |
| imagePullSecrets | list | `[]` | |
| ingressController.config.acmeCA | string | `""` | |
| ingressController.config.acmeEABKeyId | string | `""` | |
| ingressController.config.acmeEABMacKey | string | `""` | |
| ingressController.config.debug | bool | `false` | |
| ingressController.config.email | string | `""` | |
| ingressController.config.metrics | bool | `true` | |
| ingressController.config.onDemandTLS | bool | `false` | |
| ingressController.config.proxyProtocol | bool | `false` | |
| ingressController.rbac.create | bool | `true` | |
| ingressController.verbose | bool | `false` | |
| ingressController.leaseId | string | `""` | |
| ingressController.watchNamespace | string | `""` | |
| minikube | bool | `false` | |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
| podDisruptionBudget.maxUnavailable | string | `nil` | |
| podDisruptionBudget.minAvailable | int | `1` | |
| podSecurityContext | object | `{}` | |
| replicaCount | int | `2` | |
| resources | object | `{}` | |
| securityContext.allowPrivilegeEscalation | bool | `true` | |
| securityContext.capabilities.add[0] | string | `"NET_BIND_SERVICE"` | |
| securityContext.capabilities.drop[0] | string | `"ALL"` | |
| securityContext.runAsGroup | int | `0` | |
| securityContext.runAsUser | int | `0` | |
| serviceAccount.annotations | object | `{}` | |
| serviceAccount.create | bool | `true` | |
| serviceAccount.name | string | `"caddy-ingress-controller"` | |
| tolerations | list | `[]` | |

View File

@ -0,0 +1,48 @@
{{ template "chart.header" . }}
{{ template "chart.description" . }}
## TL;DR:
```bash
helm install my-release caddy-ingress-controller\
--repo https://caddyserver.github.io/ingress/ \
--namespace=caddy-system
```
## Introduction
This chart bootstraps a caddy-ingress-deployment deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
## Prerequisites
- Helm 3+
- Kubernetes 1.14+
## Installing the Chart
```bash
helm repo add caddyserver https://caddyserver.github.io/ingress/
helm install my-release caddyserver/caddy-ingress-controller --namespace=caddy-system
```
## Uninstalling the Chart
To uninstall `my-release`:
```console
$ helm uninstall my-release
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
> **Tip**: List all releases using `helm list` or start clean with `helm uninstall my-release`
## Additional Configuration
## Troubleshooting
{{ template "chart.valuesSection" . }}

View File

@ -0,0 +1,5 @@
image:
tag: test-image
ingressController:
verbose: true

View File

@ -0,0 +1,63 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "caddy-ingress-controller.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 "caddy-ingress-controller.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 "caddy-ingress-controller.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "caddy-ingress-controller.labels" -}}
helm.sh/chart: {{ include "caddy-ingress-controller.chart" . }}
{{ include "caddy-ingress-controller.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "caddy-ingress-controller.selectorLabels" -}}
app.kubernetes.io/name: {{ include "caddy-ingress-controller.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "caddy-ingress-controller.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "caddy-ingress-controller.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,31 @@
{{- if .Values.ingressController.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "caddy-ingress-controller.name" . }}-role
namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
- ""
- "networking.k8s.io"
- "coordination.k8s.io"
resources:
- ingresses
- ingresses/status
- secrets
- leases
verbs: ["*"]
- apiGroups:
- ""
resources:
- services
- pods
- nodes
- routes
- extensions
- configmaps
verbs:
- list
- get
- watch
{{- end }}

View File

@ -0,0 +1,15 @@
{{- if .Values.ingressController.rbac.create }}
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "caddy-ingress-controller.name" . }}-role-binding
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: {{ include "caddy-ingress-controller.name" . }}-role
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: {{ include "caddy-ingress-controller.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "caddy-ingress-controller.name" . }}-configmap
namespace: {{ .Release.Namespace }}
data:
{{- range keys .Values.ingressController.config | sortAlpha }}
{{ . }}: {{ get $.Values.ingressController.config . | quote }}
{{- end }}

View File

@ -0,0 +1,105 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "caddy-ingress-controller.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "caddy-ingress-controller.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "caddy-ingress-controller.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "caddy-ingress-controller.labels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "caddy-ingress-controller.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
{{- if .Values.minikube }}
hostPort: 80 # optional, required if running in minikube
{{- end }}
- name: https
containerPort: 443
protocol: TCP
{{- if .Values.minikube }}
hostPort: 443 # optional, required if running in minikube
{{- end }}
- name: metrics
containerPort: 9765
protocol: TCP
{{- if .Values.minikube }}
hostPort: 9765 # optional, required if running in minikube
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: tmp
mountPath: /tmp
args:
- -config-map={{ include "caddy-ingress-controller.name" . }}-configmap
{{- if .Values.ingressController.watchNamespace }}
- -namespace={{ .Values.ingressController.watchNamespace }}
{{- end }}
{{- if .Values.ingressController.leaseId }}
- -lease-id={{ .Values.ingressController.leaseId }}
{{- end }}
{{- if .Values.ingressController.verbose }}
- -verbose
{{- end }}
{{- if .Values.ingressController.className }}
- -class-name={{ .Values.ingressController.className }}
{{- end }}
{{- if .Values.ingressController.classNameRequired }}
- -class-name-required={{ .Values.ingressController.classNameRequired }}
{{- end }}
readinessProbe:
initialDelaySeconds: 3
periodSeconds: 10
httpGet:
port: 9765
path: /healthz
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: tmp
emptyDir: {}

View File

@ -0,0 +1,33 @@
{{- if .Values.loadBalancer.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "caddy-ingress-controller.fullname" . }}
namespace: {{ .Release.Namespace }}
{{- with .Values.loadBalancer.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "caddy-ingress-controller.labels" . | nindent 4 }}
{{- with .Values.loadBalancer.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: "LoadBalancer"
{{- if (semverCompare "<= 1.24.0" .Capabilities.KubeVersion.Version) }}
loadBalancerIP: {{ .Values.loadBalancer.loadBalancerIP }} #Deprecated in Kubernetes v1.24
{{- end }}
externalTrafficPolicy: {{ .Values.loadBalancer.externalTrafficPolicy }}
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
{{- include "caddy-ingress-controller.selectorLabels" . | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,19 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "caddy-ingress-controller.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "caddy-ingress-controller.labels" . | nindent 4 }}
spec:
{{- with .Values.podDisruptionBudget }}
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "caddy-ingress-controller.selectorLabels" . | nindent 6 }}

View File

@ -0,0 +1,35 @@
{{- if not .Values.loadBalancer.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "caddy-ingress-controller.fullname" . }}
namespace: {{ .Release.Namespace }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "caddy-ingress-controller.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
{{- if .Values.service.ipDualStack.enabled }}
ipFamilies: {{ toYaml .Values.service.ipDualStack.ipFamilies | nindent 4 }}
ipFamilyPolicy: {{ .Values.service.ipDualStack.ipFamilyPolicy }}
{{- end }}
internalTrafficPolicy: {{ .Values.service.internalTrafficPolicy }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
{{- if and (eq .Values.service.type "ClusterIP") .Values.service.clusterIP }}
clusterIP: "{{ .Values.service.clusterIP }}"
{{- end }}
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
{{- include "caddy-ingress-controller.selectorLabels" . | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "caddy-ingress-controller.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "caddy-ingress-controller.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,324 @@
{
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"replicaCount",
"minikube",
"image",
"imagePullSecrets",
"nameOverride",
"fullnameOverride",
"ingressController",
"serviceAccount",
"podAnnotations",
"podSecurityContext",
"securityContext",
"resources",
"nodeSelector",
"tolerations",
"affinity"
],
"properties": {
"replicaCount": {
"$id": "#/properties/replicaCount",
"type": "number"
},
"minikube": {
"$id": "#/properties/minikube",
"type": "boolean"
},
"image": {
"$id": "#/properties/image",
"type": "object",
"required": [
"repository",
"tag",
"pullPolicy"
],
"properties": {
"repository": {
"$id": "#/properties/image/properties/repository",
"type": "string"
},
"tag": {
"$id": "#/properties/image/properties/tag",
"type": "string"
},
"pullPolicy": {
"$id": "#/properties/image/properties/pullPolicy",
"type": "string",
"enum": [
"Always",
"IfNotPresent",
"Never"
]
}
}
},
"imagePullSecrets": {
"$id": "#/properties/imagePullSecrets",
"type": "array"
},
"nameOverride": {
"$id": "#/properties/nameOverride",
"type": "string"
},
"fullnameOverride": {
"$id": "#/properties/fullnameOverride",
"type": "string"
},
"loadBalancer": {
"$id": "#/properties/loadBalancer",
"type": "object",
"required": [
"enabled"
],
"properties": {
"enabled": {
"$id": "#/properties/loadBalancer/properties/enabled",
"type": "boolean"
},
"annotations": {
"$id": "#/properties/loadBalancer/properties/annotations",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"labels": {
"$id": "#/properties/loadBalancer/properties/labels",
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"ingressController": {
"$id": "#/properties/ingressController",
"type": "object",
"required": [
"rbac",
"config",
"watchNamespace"
],
"properties": {
"rbac": {
"$id": "#/properties/ingressController/properties/rbac",
"type": "object",
"required": [
"create"
],
"properties": {
"create": {
"$id": "#/properties/ingressController/properties/rbac/properties/create",
"type": "boolean"
}
}
},
"leaseId": {
"$id": "#/properties/ingressController/properties/leaseId",
"type": "string"
},
"config": {
"$id": "#/properties/ingressController/properties/config",
"type": "object",
"properties": {
"acmeCA": {
"$id": "#/properties/ingressController/properties/config/properties/acmeCA",
"type": "string",
"oneOf": [
{
"format": "uri"
},
{
"maxLength": 0
}
]
},
"acmeEABKeyId": {
"$id": "#/properties/ingressController/properties/config/properties/acmeEABKeyId",
"type": "string"
},
"acmeEABMacKey": {
"$id": "#/properties/ingressController/properties/config/properties/acmeEABMacKey",
"type": "string"
},
"debug": {
"$id": "#/properties/ingressController/properties/config/properties/debug",
"type": "boolean"
},
"email": {
"$id": "#/properties/ingressController/properties/config/properties/email",
"type": "string",
"oneOf": [
{
"format": "email"
},
{
"maxLength": 0
}
]
},
"experimentalSmartSort": {
"$id": "#/properties/ingressController/properties/config/properties/experimentalSmartSort",
"type": "boolean"
},
"metrics": {
"$id": "#/properties/ingressController/properties/config/properties/metrics",
"type": "boolean"
},
"proxyProtocol": {
"$id": "#/properties/ingressController/properties/config/properties/proxyProtocol",
"type": "boolean"
},
"onDemandTLS": {
"$id": "#/properties/ingressController/properties/config/properties/onDemandTLS",
"type": "boolean"
},
"onDemandAsk": {
"$id": "#/properties/ingressController/properties/config/properties/onDemandAsk",
"type": "string"
}
}
},
"verbose": {
"$id": "#/properties/ingressController/properties/verbose",
"type": "boolean"
},
"watchNamespace": {
"$id": "#/properties/ingressController/properties/watchNamespace",
"type": "string"
}
}
},
"service": {
"$id": "#/properties/service",
"type": "object",
"required": [
"type",
"ipDualStack"
],
"properties": {
"internalTrafficPolicy": {
"$id": "#/properties/service/properties/internalTrafficPolicy",
"type": "string",
"enum": [
"Cluster",
"Local"
]
},
"externalTrafficPolicy": {
"$id": "#/properties/service/properties/externalTrafficPolicy",
"type": "string",
"enum": [
"Cluster",
"Local"
]
},
"annotations": {
"$id": "#/properties/service/properties/annotations",
"type": "object"
},
"type": {
"$id": "#/properties/service/properties/type",
"type": "string",
"enum": [
"ClusterIP",
"NodePort",
"LoadBalancer",
"ExternalName"
]
},
"clusterIP": {
"$id": "#/properties/service/properties/clusterIP",
"type": "string"
},
"ipDualStack": {
"$id": "#/properties/service/properties/name",
"type": "object",
"required": [
"enabled"
],
"properties": {
"enabled": {
"$id": "#/properties/service/properties/ipDualStack/properties/enabled",
"type": "boolean"
},
"ipFamilies": {
"$id": "#/properties/service/properties/ipDualStack/properties/ipFamilies",
"type": "array",
"items": {
"type": "string",
"enum": [
"IPv4",
"IPv6"
]
}
},
"ipFamilyPolicy": {
"$id": "#/properties/service/properties/ipDualStack/properties/ipFamilyPolicy",
"type": "string",
"enum": [
"SingleStack",
"PreferDualStack",
"RequireDualStack"
]
}
}
}
}
},
"serviceAccount": {
"$id": "#/properties/serviceAccount",
"type": "object",
"required": [
"create",
"name"
],
"properties": {
"create": {
"$id": "#/properties/serviceAccount/properties/create",
"type": "boolean"
},
"name": {
"$id": "#/properties/serviceAccount/properties/name",
"type": "string"
},
"annotations": {
"$id": "#/properties/serviceAccount/properties/annotations",
"type": "object"
}
}
},
"podAnnotations": {
"$id": "#/properties/podAnnotations",
"type": "object"
},
"podSecurityContext": {
"$id": "#/properties/podSecurityContext",
"type": "object"
},
"securityContext": {
"$id": "#/properties/securityContext",
"type": "object"
},
"resources": {
"$id": "#/properties/resources",
"type": "object"
},
"nodeSelector": {
"$id": "#/properties/nodeSelector",
"type": "object"
},
"tolerations": {
"$id": "#/properties/tolerations",
"type": "array"
},
"affinity": {
"$id": "#/properties/affinity",
"type": "object"
}
}
}

View File

@ -0,0 +1,120 @@
# Default values for caddy-ingress-controller.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 2
# Use to test in minikube context
minikube: false
image:
repository: caddy/ingress
pullPolicy: IfNotPresent
tag: "v0.2.1"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# Default values for the caddy ingress controller.
ingressController:
watchNamespace: ""
verbose: false
rbac:
create: true
className: "caddy"
classNameRequired: false
leaseId: ""
config:
acmeEABKeyId: ""
acmeEABMacKey: ""
# -- Acme Server URL
acmeCA: ""
debug: false
email: ""
metrics: true
proxyProtocol: false
experimentalSmartSort: false
onDemandTLS: false
# onDemandAsk:
loadBalancer:
enabled: true
# Deprecated in Kubernetes v1.24
loadBalancerIP:
# Set to 'Local' to maintain the client's IP on inbound connections
externalTrafficPolicy:
annotations: {}
# service.beta.kubernetes.io/aws-load-balancer-type:
# service.beta.kubernetes.io/aws-load-balancer-nlb-target-type:
# service.beta.kubernetes.io/aws-load-balancer-scheme:
# service.beta.kubernetes.io/aws-load-balancer-eip-allocations:
# service.beta.kubernetes.io/aws-load-balancer-subnets:
labels: {}
service:
# Set to 'Local' to maintain the client's IP on inbound connections
externalTrafficPolicy: Cluster
# Enable internal traffic policy for the service
internalTrafficPolicy: Cluster
annotations: {}
# service.beta.kubernetes.io/aws-load-balancer-type:
# service.beta.kubernetes.io/aws-load-balancer-nlb-target-type:
# service.beta.kubernetes.io/aws-load-balancer-scheme:
# service.beta.kubernetes.io/aws-load-balancer-eip-allocations:
# service.beta.kubernetes.io/aws-load-balancer-subnets:
## Service type
type: ClusterIP
## IP address for type ClusterIP
clusterIP: ""
ipDualStack:
enabled: false
ipFamilies: ["IPv6", "IPv4"]
ipFamilyPolicy: "PreferDualStack"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: "caddy-ingress-controller"
podAnnotations: {}
podSecurityContext:
{}
# fsGroup: 2000
podDisruptionBudget:
minAvailable: 1
maxUnavailable:
securityContext:
allowPrivilegeEscalation: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
runAsUser: 0
runAsGroup: 0
resources:
{}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -0,0 +1,43 @@
package main
import (
"flag"
"strings"
"github.com/caddyserver/ingress/pkg/store"
)
func parseFlags() store.Options {
var namespace string
flag.StringVar(&namespace, "namespace", "", "the namespace that you would like to observe kubernetes ingress resources in.")
var className string
flag.StringVar(&className, "class-name", "caddy", "class name of the ingress controller")
var classNameRequired bool
flag.BoolVar(&classNameRequired, "class-name-required", false, "only allow ingress resources with a matching ingress class name")
var configMapName string
flag.StringVar(&configMapName, "config-map", "", "defines the config map name from where to load global options")
var leaseId string
flag.StringVar(&leaseId, "lease-id", "", "defines the id of this instance for certmagic lock")
var verbose bool
flag.BoolVar(&verbose, "verbose", false, "set the log level to debug")
var pluginsOrder string
flag.StringVar(&pluginsOrder, "plugins-order", "", "defines the order plugins should be used")
flag.Parse()
return store.Options{
WatchNamespace: namespace,
ClassName: className,
ClassNameRequired: classNameRequired,
ConfigMapName: configMapName,
Verbose: verbose,
LeaseId: leaseId,
PluginsOrder: strings.Split(pluginsOrder, ","),
}
}

View File

@ -0,0 +1,126 @@
package main
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/caddyserver/ingress/internal/caddy"
"github.com/caddyserver/ingress/internal/controller"
"go.uber.org/zap"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
const (
// high enough QPS to fit all expected use cases.
highQPS = 1e6
// high enough Burst to fit all expected use cases.
highBurst = 1e6
)
func createLogger(verbose bool) *zap.SugaredLogger {
prodCfg := zap.NewProductionConfig()
if verbose {
prodCfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
logger, _ := prodCfg.Build()
return logger.Sugar()
}
func main() {
// parse any flags required to configure the caddy ingress controller
cfg := parseFlags()
logger := createLogger(cfg.Verbose)
if cfg.WatchNamespace == "" {
cfg.WatchNamespace = v1.NamespaceAll
logger.Warn("-namespace flag is unset, caddy ingress controller will monitor ingress resources in all namespaces.")
}
// get client to access the kubernetes service api
kubeClient, _, err := createApiserverClient(logger)
if err != nil {
logger.Fatalf("Could not establish a connection to the Kubernetes API Server. %v", err)
}
stopCh := make(chan struct{}, 1)
c := controller.NewCaddyController(logger, kubeClient, cfg, caddy.Converter{}, stopCh)
// start the ingress controller
logger.Info("Starting the caddy ingress controller")
go c.Run()
// Listen for SIGINT and SIGTERM signals
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
<-sigs
close(stopCh)
// Let controller exit the process
select {}
}
// createApiserverClient creates a new Kubernetes REST client. We assume the
// controller runs inside Kubernetes and use the in-cluster config.
func createApiserverClient(logger *zap.SugaredLogger) (*kubernetes.Clientset, *version.Info, error) {
cfg, err := clientcmd.BuildConfigFromFlags("", "")
if err != nil {
return nil, nil, err
}
logger.Infof("Creating API client for %s", cfg.Host)
cfg.QPS = highQPS
cfg.Burst = highBurst
cfg.ContentType = "application/vnd.kubernetes.protobuf"
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, nil, err
}
// The client may fail to connect to the API server on the first request
defaultRetry := wait.Backoff{
Steps: 10,
Duration: 1 * time.Second,
Factor: 1.5,
Jitter: 0.1,
}
var v *version.Info
var retries int
var lastErr error
err = wait.ExponentialBackoff(defaultRetry, func() (bool, error) {
v, err = client.Discovery().ServerVersion()
if err == nil {
return true, nil
}
lastErr = err
logger.Infof("Unexpected error discovering Kubernetes version (attempt %v): %v", retries, err)
retries++
return false, nil
})
// err is returned in case of timeout in the exponential backoff (ErrWaitTimeout)
if err != nil {
return nil, nil, lastErr
}
if retries > 0 {
logger.Warnf("Initial connection to the Kubernetes API server was retried %d times.", retries)
}
return client, v, nil
}

View File

@ -0,0 +1,3 @@
# See https://github.com/helm/chart-testing#configuration
all: true
helm-extra-args: --timeout 300s

View File

@ -0,0 +1,152 @@
module github.com/caddyserver/ingress
go 1.24.0
require (
github.com/caddyserver/caddy/v2 v2.9.1
github.com/caddyserver/certmagic v0.22.2
github.com/google/uuid v1.6.0
github.com/mholt/acmez/v3 v3.1.1
github.com/mitchellh/mapstructure v1.5.0
github.com/pires/go-proxyproto v0.8.0
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.27.0
gopkg.in/go-playground/pool.v3 v3.1.1
k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.3
)
require (
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/libdns/libdns v0.2.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.21.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/slackhq/nebula v1.7.2 // indirect
github.com/smallstep/certificates v0.26.1 // indirect
github.com/smallstep/nosql v0.6.1 // indirect
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.3.9 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

View File

@ -0,0 +1,806 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.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.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0=
github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/caddyserver/caddy/v2 v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
github.com/caddyserver/caddy/v2 v2.9.1/go.mod h1:ImUELya2el1FDVp3ahnSO2iH1or1aHxlQEQxd/spP68=
github.com/caddyserver/certmagic v0.22.2 h1:qzZURXlrxwR5m25/jpvVeEyJHeJJMvAwe5zlMufOTQk=
github.com/caddyserver/certmagic v0.22.2/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.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/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
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-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/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/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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
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/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8=
github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
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.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.1.1 h1:Jh+9uKHkPxUJdxM16q5mOr+G2V0aqkuFtNA28ihCxhQ=
github.com/mholt/acmez/v3 v3.1.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
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-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slackhq/nebula v1.7.2 h1:Rko1Mlksz/nC0c919xjGpB8uOSrTJ5e6KPgZx+lVfYw=
github.com/slackhq/nebula v1.7.2/go.mod h1:cnaoahkUipDs1vrNoIszyp0QPRIQN9Pm68ppQEW1Fhg=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o=
github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis=
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y=
github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y=
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg=
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw=
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY=
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/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-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/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-20190313220215-9f648a60d977/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-20190813141303-74dc4d7220e7/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/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-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210615035016-665e8c7367d1/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-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/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-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
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-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/pool.v3 v3.1.1 h1:4Qcj91IsYTpIeRhe/eo6Fz+w6uKWPEghx8vHFTYMfhw=
gopkg.in/go-playground/pool.v3 v3.1.1/go.mod h1:pUAGBximS/hccTTSzEop6wvvQhVa3QPDFFW+8REdutg=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View File

@ -0,0 +1,26 @@
package caddy
import (
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
// Load default plugins
_ "github.com/caddyserver/ingress/internal/caddy/global"
_ "github.com/caddyserver/ingress/internal/caddy/ingress"
)
type Converter struct{}
func (c Converter) ConvertToCaddyConfig(store *store.Store) (interface{}, error) {
cfg := converter.NewConfig()
for _, p := range converter.Plugins(store.Options.PluginsOrder) {
if m, ok := p.(converter.GlobalMiddleware); ok {
err := m.GlobalHandler(cfg, store)
if err != nil {
return cfg, err
}
}
}
return cfg, nil
}

View File

@ -0,0 +1,36 @@
package caddy
import (
"encoding/json"
"os"
"testing"
"github.com/caddyserver/ingress/pkg/store"
"github.com/stretchr/testify/require"
)
func TestConvertToCaddyConfig(t *testing.T) {
tests := []struct {
name string
expectedConfigPath string
}{
{
name: "default",
expectedConfigPath: "./test_data/default.json",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg, err := Converter{}.ConvertToCaddyConfig(store.NewStore(store.Options{}, &store.PodInfo{}))
require.NoError(t, err)
cfgJson, err := json.Marshal(cfg)
require.NoError(t, err)
expectedCfg, err := os.ReadFile(test.expectedConfigPath)
require.NoError(t, err)
require.JSONEq(t, string(expectedCfg), string(cfgJson))
})
}
}

View File

@ -0,0 +1,88 @@
package global
import (
"encoding/json"
caddy2 "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
"github.com/mholt/acmez/v3/acme"
)
type ConfigMapPlugin struct{}
func init() {
converter.RegisterPlugin(ConfigMapPlugin{})
}
func (p ConfigMapPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "configmap",
New: func() converter.Plugin { return new(ConfigMapPlugin) },
}
}
func (p ConfigMapPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
cfgMap := store.ConfigMap
tlsApp := config.GetTLSApp()
httpServer := config.GetHTTPServer()
if cfgMap.Debug {
config.Logging.Logs = map[string]*caddy2.CustomLog{"default": {BaseLog: caddy2.BaseLog{Level: "DEBUG"}}}
}
if cfgMap.AcmeCA != "" || cfgMap.Email != "" {
acmeIssuer := caddytls.ACMEIssuer{}
if cfgMap.AcmeCA != "" {
acmeIssuer.CA = cfgMap.AcmeCA
}
if cfgMap.AcmeEABKeyId != "" && cfgMap.AcmeEABMacKey != "" {
acmeIssuer.ExternalAccount = &acme.EAB{
KeyID: cfgMap.AcmeEABKeyId,
MACKey: cfgMap.AcmeEABMacKey,
}
}
if cfgMap.Email != "" {
acmeIssuer.Email = cfgMap.Email
}
var onDemandConfig *caddytls.OnDemandConfig
if cfgMap.OnDemandTLS {
onDemandConfig = &caddytls.OnDemandConfig{
Ask: cfgMap.OnDemandAsk,
}
}
tlsApp.Automation = &caddytls.AutomationConfig{
OnDemand: onDemandConfig,
OCSPCheckInterval: cfgMap.OCSPCheckInterval,
Policies: []*caddytls.AutomationPolicy{
{
IssuersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(acmeIssuer, "module", "acme", nil),
},
OnDemand: cfgMap.OnDemandTLS,
},
},
}
}
if cfgMap.ProxyProtocol {
httpServer.ListenerWrappersRaw = []json.RawMessage{
json.RawMessage(`{"wrapper":"proxy_protocol"}`),
json.RawMessage(`{"wrapper":"tls"}`),
}
}
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(ConfigMapPlugin{})
)

View File

@ -0,0 +1,48 @@
package global
import (
"encoding/json"
"net/http"
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
)
type HealthzPlugin struct{}
func (p HealthzPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "healthz",
Priority: -20,
New: func() converter.Plugin { return new(HealthzPlugin) },
}
}
func init() {
converter.RegisterPlugin(HealthzPlugin{})
}
func (p HealthzPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
healthzHandler := caddyhttp.StaticResponse{StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK))}
healthzRoute := caddyhttp.Route{
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(healthzHandler, "handler", healthzHandler.CaddyModule().ID.Name(), nil),
},
MatcherSetsRaw: []caddy.ModuleMap{{
"path": caddyconfig.JSON(caddyhttp.MatchPath{"/healthz"}, nil),
}},
}
config.GetMetricsServer().Routes = append(config.GetMetricsServer().Routes, healthzRoute)
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(HealthzPlugin{})
)

View File

@ -0,0 +1,70 @@
package global
import (
"encoding/json"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
)
type IngressPlugin struct{}
func (p IngressPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress",
New: func() converter.Plugin { return new(IngressPlugin) },
}
}
func init() {
converter.RegisterPlugin(IngressPlugin{})
}
func (p IngressPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
ingressHandlers := make([]converter.IngressMiddleware, 0)
for _, plugin := range converter.Plugins(store.Options.PluginsOrder) {
if m, ok := plugin.(converter.IngressMiddleware); ok {
ingressHandlers = append(ingressHandlers, m)
}
}
// create a server route for each ingress route
var routes caddyhttp.RouteList
for _, ing := range store.Ingresses {
for _, rule := range ing.Spec.Rules {
for _, path := range rule.HTTP.Paths {
r := &caddyhttp.Route{
HandlersRaw: []json.RawMessage{},
MatcherSetsRaw: []caddy.ModuleMap{},
}
for _, middleware := range ingressHandlers {
newRoute, err := middleware.IngressHandler(converter.IngressMiddlewareInput{
Config: config,
Store: store,
Ingress: ing,
Rule: rule,
Path: path,
Route: r,
})
if err != nil {
return err
}
r = newRoute
}
routes = append(routes, *r)
}
}
}
config.GetHTTPServer().Routes = routes
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(IngressPlugin{})
)

View File

@ -0,0 +1,77 @@
package global
import (
"encoding/json"
"sort"
"strings"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
)
type IngressSortPlugin struct{}
func (p IngressSortPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress_sort",
// Must go after ingress are configured
Priority: -2,
New: func() converter.Plugin { return new(IngressSortPlugin) },
}
}
func init() {
converter.RegisterPlugin(IngressSortPlugin{})
}
func getFirstItemFromJSON(data json.RawMessage) string {
var arr []string
err := json.Unmarshal(data, &arr)
if err != nil {
return ""
}
return arr[0]
}
func sortRoutes(routes caddyhttp.RouteList) {
sort.SliceStable(routes, func(i, j int) bool {
iPath := getFirstItemFromJSON(routes[i].MatcherSetsRaw[0]["path"])
jPath := getFirstItemFromJSON(routes[j].MatcherSetsRaw[0]["path"])
iPrefixed := strings.HasSuffix(iPath, "*")
jPrefixed := strings.HasSuffix(jPath, "*")
// If both same type check by length
if iPrefixed == jPrefixed {
return len(jPath) < len(iPath)
}
// Empty path will be moved last
if jPath == "" || iPath == "" {
return jPath == ""
}
// j path is exact so should go first
return jPrefixed
})
}
// GlobalHandler in IngressSortPlugin tries to sort routes to have the less conflict.
//
// It only supports basic conflicts for now. It doesn't support multiple matchers in the same route
// nor multiple path/host in the matcher. It shouldn't be an issue with the ingress.matcher plugin.
// Sort will prioritize exact paths then prefix paths and finally empty paths.
// When 2 exacts paths or 2 prefixed paths are on the same host, we choose the longer first.
func (p IngressSortPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
if !store.ConfigMap.ExperimentalSmartSort {
return nil
}
routes := config.GetHTTPServer().Routes
sortRoutes(routes)
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(IngressSortPlugin{})
)

View File

@ -0,0 +1,109 @@
package global
import (
"encoding/json"
"reflect"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestIngressSort(t *testing.T) {
tests := []struct {
name string
routes []struct {
id int
path string
}
expect []int
}{
{
name: "multiple exact paths",
routes: []struct {
id int
path string
}{
{id: 0, path: "/path/a"},
{id: 1, path: "/path/"},
{id: 2, path: "/other"},
},
expect: []int{0, 1, 2},
},
{
name: "multiple prefix paths",
routes: []struct {
id int
path string
}{
{id: 0, path: "/path/*"},
{id: 1, path: "/path/auth/*"},
{id: 2, path: "/other/*"},
{id: 3, path: "/login/*"},
},
expect: []int{1, 2, 3, 0},
},
{
name: "mixed exact and prefixed",
routes: []struct {
id int
path string
}{
{id: 0, path: "/path/*"},
{id: 1, path: "/path/auth/"},
{id: 2, path: "/path/v2/*"},
{id: 3, path: "/path/new"},
},
expect: []int{1, 3, 2, 0},
},
{
name: "mixed exact, prefix and empty",
routes: []struct {
id int
path string
}{
{id: 0, path: "/path/*"},
{id: 1, path: ""},
{id: 2, path: "/path/v2/*"},
{id: 3, path: "/path/new"},
{id: 4, path: ""},
},
expect: []int{3, 2, 0, 1, 4},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
routes := []caddyhttp.Route{}
for _, route := range test.routes {
match := caddy.ModuleMap{}
match["id"] = caddyconfig.JSON(route.id, nil)
if route.path != "" {
match["path"] = caddyconfig.JSON(caddyhttp.MatchPath{route.path}, nil)
}
r := caddyhttp.Route{MatcherSetsRaw: []caddy.ModuleMap{match}}
routes = append(routes, r)
}
sortRoutes(routes)
var got []int
for i := range test.expect {
var currentId int
err := json.Unmarshal(routes[i].MatcherSetsRaw[0]["id"], &currentId)
if err != nil {
t.Fatalf("error unmarshaling id for i %v, %v", i, err)
}
got = append(got, currentId)
}
if !reflect.DeepEqual(test.expect, got) {
t.Errorf("expected order to match: got %v, expected %v, %s", got, test.expect, routes[1].MatcherSetsRaw)
}
})
}
}

View File

@ -0,0 +1,43 @@
package global
import (
"encoding/json"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
)
type MetricsPlugin struct{}
func (p MetricsPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "metrics",
New: func() converter.Plugin { return new(MetricsPlugin) },
}
}
func init() {
converter.RegisterPlugin(MetricsPlugin{})
}
func (p MetricsPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
if store.ConfigMap.Metrics {
metricsRoute := caddyhttp.Route{
HandlersRaw: []json.RawMessage{json.RawMessage(`{ "handler": "metrics" }`)},
MatcherSetsRaw: []caddy.ModuleMap{{
"path": caddyconfig.JSON(caddyhttp.MatchPath{"/metrics"}, nil),
}},
}
config.GetMetricsServer().Routes = append(config.GetMetricsServer().Routes, metricsRoute)
}
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(MetricsPlugin{})
)

View File

@ -0,0 +1,36 @@
package global
import (
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
)
type SecretsStorePlugin struct{}
func (p SecretsStorePlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "secrets_store",
New: func() converter.Plugin { return new(SecretsStorePlugin) },
}
}
func init() {
converter.RegisterPlugin(SecretsStorePlugin{})
}
func (p SecretsStorePlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
config.Storage = converter.Storage{
System: "secret_store",
StorageValues: converter.StorageValues{
Namespace: store.CurrentPod.Namespace,
LeaseId: store.Options.LeaseId,
},
}
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(SecretsStorePlugin{})
)

View File

@ -0,0 +1,53 @@
package global
import (
"encoding/json"
"slices"
"github.com/caddyserver/ingress/internal/controller"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
)
type TLSPlugin struct{}
func (p TLSPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "tls",
New: func() converter.Plugin { return new(TLSPlugin) },
}
}
func init() {
converter.RegisterPlugin(TLSPlugin{})
}
func (p TLSPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
tlsApp := config.GetTLSApp()
httpServer := config.GetHTTPServer()
var hosts []string
// Get all Hosts subject to custom TLS certs
for _, ing := range store.Ingresses {
for _, tlsRule := range ing.Spec.TLS {
for _, h := range tlsRule.Hosts {
if !slices.Contains(hosts, h) {
hosts = append(hosts, h)
}
}
}
}
if len(hosts) > 0 {
tlsApp.CertificatesRaw["load_folders"] = json.RawMessage(`["` + controller.CertFolder + `"]`)
// do not manage certificates for those hosts
httpServer.AutoHTTPS.SkipCerts = hosts
}
return nil
}
// Interface guards
var (
_ = converter.GlobalMiddleware(TLSPlugin{})
)

View File

@ -0,0 +1,211 @@
package global
import (
"testing"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/caddyserver/ingress/pkg/store"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestIngressTlsSkipCertificates(t *testing.T) {
testCases := []struct {
desc string
skippedCertsDomains []string
ingresses []*networkingv1.Ingress
}{
{
desc: "No ingress registered",
skippedCertsDomains: []string{},
ingresses: []*networkingv1.Ingress{},
},
{
desc: "One ingress registered with certificate with one domain",
skippedCertsDomains: []string{"domain1.tld"},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld"},
}},
},
},
},
},
{
desc: "One ingress registered with certificate with multiple domains",
skippedCertsDomains: []string{"domain1.tld", "domain2.tld", "domain3.tld"},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld", "domain2.tld", "domain3.tld"},
}},
},
},
},
},
{
desc: "Two ingress registered with certificate one domain each",
skippedCertsDomains: []string{"domain1.tld", "domain2.tld"},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld"},
}},
},
},
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("second"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain2.tld"},
}},
},
},
},
},
{
desc: "Two ingress registered with certificate the same domain",
skippedCertsDomains: []string{"domain1.tld"},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld"},
}},
},
},
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("second"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld"},
}},
},
},
},
},
{
desc: "Two ingress registered with certificate with multiple domains each",
skippedCertsDomains: []string{"domain1a.tld", "domain1b.tld", "domain2a.tld", "domain2b.tld"},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1a.tld", "domain1b.tld"},
}},
},
},
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("second"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain2a.tld", "domain2b.tld"},
}},
},
},
},
},
{
desc: "Two ingress registered with certificate with multiple domains each and partial domain overlap",
skippedCertsDomains: []string{"domain1.tld", "domain2a.tld", "domain2b.tld"},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld", "domain2a.tld"},
}},
},
},
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("second"),
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{{
Hosts: []string{"domain1.tld", "domain2b.tld"},
}},
},
},
},
},
{
desc: "One ingress registered without certificate",
skippedCertsDomains: []string{},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{},
},
},
},
{
desc: "Two ingresses registered without certificate",
skippedCertsDomains: []string{},
ingresses: []*networkingv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("first"),
},
Spec: networkingv1.IngressSpec{},
},
{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID("second"),
},
Spec: networkingv1.IngressSpec{},
},
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
tp := TLSPlugin{}
c := converter.NewConfig()
s := store.NewStore(store.Options{}, &store.PodInfo{})
for _, ing := range tC.ingresses {
s.AddIngress(ing)
}
tp.GlobalHandler(c, s)
toSkip := c.GetHTTPServer().AutoHTTPS.SkipCerts
assert.ElementsMatch(t, toSkip, tC.skippedCertsDomains, "List of certificate to skip don't match expectation")
})
}
}

View File

@ -0,0 +1,28 @@
package ingress
import v1 "k8s.io/api/networking/v1"
const (
annotationPrefix = "caddy.ingress.kubernetes.io"
rewriteToAnnotation = "rewrite-to"
rewriteStripPrefixAnnotation = "rewrite-strip-prefix"
disableSSLRedirect = "disable-ssl-redirect"
backendProtocol = "backend-protocol"
insecureSkipVerify = "insecure-skip-verify"
permanentRedirectAnnotation = "permanent-redirect"
permanentRedirectCodeAnnotation = "permanent-redirect-code"
temporaryRedirectAnnotation = "temporal-redirect"
trustedProxies = "trusted-proxies"
)
func getAnnotation(ing *v1.Ingress, rule string) string {
return ing.Annotations[annotationPrefix+"/"+rule]
}
func getAnnotationBool(ing *v1.Ingress, rule string, def bool) bool {
val := getAnnotation(ing, rule)
if val == "" {
return def
}
return val == "true"
}

View File

@ -0,0 +1,52 @@
package ingress
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
v1 "k8s.io/api/networking/v1"
)
type MatcherPlugin struct{}
func (p MatcherPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress.matcher",
New: func() converter.Plugin { return new(MatcherPlugin) },
}
}
// IngressHandler Generate matchers for the route.
func (p MatcherPlugin) IngressHandler(input converter.IngressMiddlewareInput) (*caddyhttp.Route, error) {
match := caddy.ModuleMap{}
if getAnnotation(input.Ingress, disableSSLRedirect) != "true" {
match["protocol"] = caddyconfig.JSON(caddyhttp.MatchProtocol("https"), nil)
}
if input.Rule.Host != "" {
match["host"] = caddyconfig.JSON(caddyhttp.MatchHost{input.Rule.Host}, nil)
}
if input.Path.Path != "" {
p := input.Path.Path
if *input.Path.PathType == v1.PathTypePrefix {
p += "*"
}
match["path"] = caddyconfig.JSON(caddyhttp.MatchPath{p}, nil)
}
input.Route.MatcherSetsRaw = append(input.Route.MatcherSetsRaw, match)
return input.Route, nil
}
func init() {
converter.RegisterPlugin(MatcherPlugin{})
}
// Interface guards
var (
_ = converter.IngressMiddleware(MatcherPlugin{})
)

View File

@ -0,0 +1,82 @@
package ingress
import (
"fmt"
"net/http"
"strconv"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
)
type RedirectPlugin struct{}
func (p RedirectPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress.redirect",
Priority: 10,
New: func() converter.Plugin { return new(RedirectPlugin) },
}
}
// IngressHandler Converts redirect annotations to static_response handler
func (p RedirectPlugin) IngressHandler(input converter.IngressMiddlewareInput) (*caddyhttp.Route, error) {
ing := input.Ingress
permanentRedir := getAnnotation(ing, permanentRedirectAnnotation)
temporaryRedir := getAnnotation(ing, temporaryRedirectAnnotation)
var code string = "301"
var redirectTo string = ""
// Don't allow both redirect annotations to be set
if permanentRedir != "" && temporaryRedir != "" {
return nil, fmt.Errorf("cannot use permanent-redirect annotation with temporal-redirect")
}
if permanentRedir != "" {
redirectCode := getAnnotation(ing, permanentRedirectCodeAnnotation)
if redirectCode != "" {
codeInt, err := strconv.Atoi(redirectCode)
if err != nil {
return nil, fmt.Errorf("not a supported redirection code type or not a valid integer: '%s'", redirectCode)
}
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
return nil, fmt.Errorf("redirection code not in the 3xx range or 401: '%v'", codeInt)
}
code = redirectCode
}
redirectTo = permanentRedir
}
if temporaryRedir != "" {
code = "302"
redirectTo = temporaryRedir
}
if redirectTo != "" {
handler := caddyconfig.JSONModuleObject(
caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code),
Headers: http.Header{"Location": []string{redirectTo}},
},
"handler", "static_response", nil,
)
input.Route.HandlersRaw = append(input.Route.HandlersRaw, handler)
}
return input.Route, nil
}
func init() {
converter.RegisterPlugin(RedirectPlugin{})
}
// Interface guards
var (
_ = converter.IngressMiddleware(RedirectPlugin{})
)

View File

@ -0,0 +1,135 @@
package ingress
import (
"encoding/json"
"os"
"testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/stretchr/testify/assert"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestRedirectConvertToCaddyConfig(t *testing.T) {
rp := RedirectPlugin{}
tests := []struct {
name string
expectedConfigPath string
expectedError string
annotations map[string]string
}{
{
name: "Check permanent redirect without any specific redirect code",
expectedConfigPath: "test_data/redirect_default.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
},
},
{
name: "Check permanent redirect with custom redirect code",
expectedConfigPath: "test_data/redirect_custom_code.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "308",
},
},
{
name: "Check permanent redirect with 401 as redirect code",
expectedConfigPath: "test_data/redirect_401.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "401",
},
},
{
name: "Check temporary redirect",
expectedConfigPath: "test_data/redirect_temporary.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/temporal-redirect": "http://example.com",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := converter.IngressMiddlewareInput{
Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: test.annotations,
},
},
Route: &caddyhttp.Route{},
}
route, err := rp.IngressHandler(input)
assert.NoError(t, err, "failed to generate ingress route")
expectedCfg, err := os.ReadFile(test.expectedConfigPath)
assert.NoError(t, err, "failed to find config file for comparison")
cfgJson, err := json.Marshal(&route)
assert.NoError(t, err, "failed to marshal route to JSON")
assert.JSONEq(t, string(cfgJson), string(expectedCfg))
})
}
}
func TestMisconfiguredRedirectConvertToCaddyConfig(t *testing.T) {
rp := RedirectPlugin{}
tests := []struct {
name string
expectedError string
annotations map[string]string
}{
{
name: "Check permanent redirect with invalid custom redirect code",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "502",
},
expectedError: "redirection code not in the 3xx range or 401: '502'",
},
{
name: "Check permanent redirect with invalid custom redirect code string",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "randomstring",
},
expectedError: "not a supported redirection code type or not a valid integer: 'randomstring'",
},
{
name: "Check if both permanent and temporary redirection annotations are set",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/temporal-redirect": "http://example2.com",
},
expectedError: "cannot use permanent-redirect annotation with temporal-redirect",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := converter.IngressMiddlewareInput{
Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: test.annotations,
},
},
Route: &caddyhttp.Route{},
}
route, err := rp.IngressHandler(input)
if assert.Error(t, err, "expected an error while generating the ingress route") {
assert.EqualError(t, err, test.expectedError)
}
cfgJson, err := json.Marshal(&route)
assert.NoError(t, err, "failed to marshal route to JSON")
assert.JSONEq(t, string(cfgJson), "null")
})
}
}

View File

@ -0,0 +1,103 @@
package ingress
import (
"fmt"
"net/netip"
"strings"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/caddyserver/ingress/pkg/converter"
)
type ReverseProxyPlugin struct{}
func (p ReverseProxyPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress.reverseproxy",
// Should always go last by default
Priority: -10,
New: func() converter.Plugin { return new(ReverseProxyPlugin) },
}
}
// IngressHandler Add a reverse proxy handler to the route
func (p ReverseProxyPlugin) IngressHandler(input converter.IngressMiddlewareInput) (*caddyhttp.Route, error) {
path := input.Path
ing := input.Ingress
backendProtocol := strings.ToLower(getAnnotation(ing, backendProtocol))
trustedProxiesAnnotation := strings.ToLower(getAnnotation(ing, trustedProxies))
// TODO :-
// when setting the upstream url we should bypass kube-dns and get the ip address of
// the pod for the deployment we are proxying to so that we can proxy to that ip address port.
// this is good for session affinity and increases performance.
clusterHostName := fmt.Sprintf("%v.%v.svc.cluster.local:%d", path.Backend.Service.Name, ing.Namespace, path.Backend.Service.Port.Number)
transport := &reverseproxy.HTTPTransport{}
if backendProtocol == "https" {
transport.TLS = &reverseproxy.TLSConfig{
InsecureSkipVerify: getAnnotationBool(ing, insecureSkipVerify, true),
}
}
var err error
var parsedProxies []string
if trustedProxiesAnnotation != "" {
trustedProxies := strings.Split(trustedProxiesAnnotation, ",")
parsedProxies, err = parseTrustedProxies(trustedProxies)
if err != nil {
return nil, err
}
}
handler := reverseproxy.Handler{
TransportRaw: caddyconfig.JSONModuleObject(transport, "protocol", "http", nil),
Upstreams: reverseproxy.UpstreamPool{
{Dial: clusterHostName},
},
TrustedProxies: parsedProxies,
}
handlerModule := caddyconfig.JSONModuleObject(
handler,
"handler",
"reverse_proxy",
nil,
)
input.Route.HandlersRaw = append(input.Route.HandlersRaw, handlerModule)
return input.Route, nil
}
// Copied from https://github.com/caddyserver/caddy/blob/21af88fefc9a8239a024f635f1c6fdd9defd7eb7/modules/caddyhttp/reverseproxy/reverseproxy.go#L270-L286
func parseTrustedProxies(trustedProxies []string) (parsedProxies []string, err error) {
for _, trustedProxy := range trustedProxies {
trustedProxy = strings.TrimSpace(trustedProxy)
if strings.Contains(trustedProxy, "/") {
ipNet, err := netip.ParsePrefix(trustedProxy)
if err != nil {
return nil, fmt.Errorf("failed to parse IP: %q", trustedProxy)
}
parsedProxies = append(parsedProxies, ipNet.String())
} else {
ipAddr, err := netip.ParseAddr(trustedProxy)
if err != nil {
return nil, fmt.Errorf("failed to parse IP: %q", trustedProxy)
}
ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
parsedProxies = append(parsedProxies, ipNew.String())
}
}
return parsedProxies, nil
}
func init() {
converter.RegisterPlugin(ReverseProxyPlugin{})
}
// Interface guards
var (
_ = converter.IngressMiddleware(ReverseProxyPlugin{})
)

View File

@ -0,0 +1,168 @@
package ingress
import (
"encoding/json"
"os"
"testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/stretchr/testify/require"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestTrustedProxesConvertToCaddyConfig(t *testing.T) {
rpp := ReverseProxyPlugin{}
tests := []struct {
name string
annotations map[string]string
expectedConfigPath string
}{
{
name: "ipv4 trusted proxies",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "192.168.1.0, 10.0.0.1",
},
expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv4.json",
},
{
name: "ipv4 trusted proxies wit subnet",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "192.168.1.0/16,10.0.0.1/8",
},
expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv4_subnet.json",
},
{
name: "ipv6 trusted proxies",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::1, 2001:db8::5",
},
expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv6.json",
},
{
name: "ipv6 trusted proxies",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::1/36,2001:db8::5/60",
},
expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv6_subnet.json",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := converter.IngressMiddlewareInput{
Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: test.annotations,
Namespace: "namespace",
},
},
Path: networkingv1.HTTPIngressPath{
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "svcName",
Port: networkingv1.ServiceBackendPort{Number: 80},
},
},
},
Route: &caddyhttp.Route{},
}
route, err := rpp.IngressHandler(input)
require.NoError(t, err)
expectedCfg, err := os.ReadFile(test.expectedConfigPath)
require.NoError(t, err)
cfgJson, err := json.Marshal(&route)
require.NoError(t, err)
require.JSONEq(t, string(expectedCfg), string(cfgJson))
})
}
}
func TestMisconfiguredTrustedProxiesConvertToCaddyConfig(t *testing.T) {
rpp := ReverseProxyPlugin{}
tests := []struct {
name string
annotations map[string]string
expectedError string
}{
{
name: "invalid ipv4 trusted proxy",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "999.999.999.999",
},
expectedError: `failed to parse IP: "999.999.999.999"`,
},
{
name: "invalid ipv4 with subnet trusted proxy",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "999.999.999.999/32",
},
expectedError: `failed to parse IP: "999.999.999.999/32"`,
},
{
name: "invalid subnet for ipv4 trusted proxy",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "10.0.0.0/100",
},
expectedError: `failed to parse IP: "10.0.0.0/100"`,
},
{
name: "invalid ipv6 trusted proxy",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::g",
},
expectedError: `failed to parse IP: "2001:db8::g"`,
},
{
name: "invalid ipv6 with subnet trusted proxy",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::g/128",
},
expectedError: `failed to parse IP: "2001:db8::g/128"`,
},
{
name: "invalid subnet for ipv6 trusted proxy",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::/200",
},
expectedError: `failed to parse IP: "2001:db8::/200"`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := converter.IngressMiddlewareInput{
Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: test.annotations,
Namespace: "namespace",
},
},
Path: networkingv1.HTTPIngressPath{
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "svcName",
Port: networkingv1.ServiceBackendPort{Number: 80},
},
},
},
Route: &caddyhttp.Route{},
}
route, err := rpp.IngressHandler(input)
require.EqualError(t, err, test.expectedError)
cfgJson, err := json.Marshal(&route)
require.NoError(t, err)
require.JSONEq(t, string(cfgJson), "null")
})
}
}

View File

@ -0,0 +1,53 @@
package ingress
import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/ingress/pkg/converter"
)
type RewritePlugin struct{}
func (p RewritePlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress.rewrite",
Priority: 10,
New: func() converter.Plugin { return new(RewritePlugin) },
}
}
// IngressHandler Converts rewrite annotations to rewrite handler
func (p RewritePlugin) IngressHandler(input converter.IngressMiddlewareInput) (*caddyhttp.Route, error) {
ing := input.Ingress
rewriteTo := getAnnotation(ing, rewriteToAnnotation)
if rewriteTo != "" {
handler := caddyconfig.JSONModuleObject(
rewrite.Rewrite{URI: rewriteTo},
"handler", "rewrite", nil,
)
input.Route.HandlersRaw = append(input.Route.HandlersRaw, handler)
}
rewriteStripPrefix := getAnnotation(ing, rewriteStripPrefixAnnotation)
if rewriteStripPrefix != "" {
handler := caddyconfig.JSONModuleObject(
rewrite.Rewrite{StripPathPrefix: rewriteStripPrefix},
"handler", "rewrite", nil,
)
input.Route.HandlersRaw = append(input.Route.HandlersRaw, handler)
}
return input.Route, nil
}
func init() {
converter.RegisterPlugin(RewritePlugin{})
}
// Interface guards
var (
_ = converter.IngressMiddleware(RewritePlugin{})
)

View File

@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 401
}
]
}

View File

@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 308
}
]
}

View File

@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 301
}
]
}

View File

@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 302
}
]
}

View File

@ -0,0 +1,19 @@
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http"
},
"trusted_proxies": [
"192.168.1.0/32",
"10.0.0.1/32"
],
"upstreams": [
{
"dial": "svcName.namespace.svc.cluster.local:80"
}
]
}
]
}

View File

@ -0,0 +1,19 @@
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http"
},
"trusted_proxies": [
"192.168.1.0/16",
"10.0.0.1/8"
],
"upstreams": [
{
"dial": "svcName.namespace.svc.cluster.local:80"
}
]
}
]
}

View File

@ -0,0 +1,19 @@
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http"
},
"trusted_proxies": [
"2001:db8::1/128",
"2001:db8::5/128"
],
"upstreams": [
{
"dial": "svcName.namespace.svc.cluster.local:80"
}
]
}
]
}

View File

@ -0,0 +1,19 @@
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http"
},
"trusted_proxies": [
"2001:db8::1/36",
"2001:db8::5/60"
],
"upstreams": [
{
"dial": "svcName.namespace.svc.cluster.local:80"
}
]
}
]
}

View File

@ -0,0 +1,27 @@
{
"admin": {},
"storage": { "module": "secret_store", "namespace": "", "leaseId": "" },
"apps": {
"http": {
"servers": {
"ingress_server": {
"listen": [":80", ":443"],
"tls_connection_policies": [{}],
"automatic_https": {}
},
"metrics_server": {
"listen": [":9765"],
"routes": [
{
"match": [{ "path": ["/healthz"] }],
"handle": [{ "handler": "static_response", "status_code": 200 }]
}
],
"automatic_https": { "disable": true }
}
}
},
"tls": {}
},
"logging": {}
}

View File

@ -0,0 +1,71 @@
package controller
import (
"github.com/caddyserver/ingress/pkg/store"
v1 "k8s.io/api/core/v1"
)
// ConfigMapAddedAction provides an implementation of the action interface.
type ConfigMapAddedAction struct {
resource *v1.ConfigMap
}
// ConfigMapUpdatedAction provides an implementation of the action interface.
type ConfigMapUpdatedAction struct {
resource *v1.ConfigMap
oldResource *v1.ConfigMap
}
// ConfigMapDeletedAction provides an implementation of the action interface.
type ConfigMapDeletedAction struct {
resource *v1.ConfigMap
}
// onConfigMapAdded runs when a configmap is added to the namespace.
func (c *CaddyController) onConfigMapAdded(obj *v1.ConfigMap) {
c.syncQueue.Add(ConfigMapAddedAction{
resource: obj,
})
}
// onConfigMapUpdated is run when a configmap is updated in the namespace.
func (c *CaddyController) onConfigMapUpdated(old *v1.ConfigMap, new *v1.ConfigMap) {
c.syncQueue.Add(ConfigMapUpdatedAction{
resource: new,
oldResource: old,
})
}
// onConfigMapDeleted is run when a configmap is deleted from the namespace.
func (c *CaddyController) onConfigMapDeleted(obj *v1.ConfigMap) {
c.syncQueue.Add(ConfigMapDeletedAction{
resource: obj,
})
}
func (r ConfigMapAddedAction) handle(c *CaddyController) error {
c.logger.Infof("ConfigMap created (%s/%s)", r.resource.Namespace, r.resource.Name)
cfg, err := store.ParseConfigMap(r.resource)
if err == nil {
c.resourceStore.ConfigMap = cfg
}
return err
}
func (r ConfigMapUpdatedAction) handle(c *CaddyController) error {
c.logger.Infof("ConfigMap updated (%s/%s)", r.resource.Namespace, r.resource.Name)
cfg, err := store.ParseConfigMap(r.resource)
if err == nil {
c.resourceStore.ConfigMap = cfg
}
return err
}
func (r ConfigMapDeletedAction) handle(c *CaddyController) error {
c.logger.Infof("ConfigMap deleted (%s/%s)", r.resource.Namespace, r.resource.Name)
c.resourceStore.ConfigMap = nil
return nil
}

View File

@ -0,0 +1,70 @@
package controller
import (
v1 "k8s.io/api/networking/v1"
)
// IngressAddedAction provides an implementation of the action interface.
type IngressAddedAction struct {
resource *v1.Ingress
}
// IngressUpdatedAction provides an implementation of the action interface.
type IngressUpdatedAction struct {
resource *v1.Ingress
oldResource *v1.Ingress
}
// IngressDeletedAction provides an implementation of the action interface.
type IngressDeletedAction struct {
resource *v1.Ingress
}
// onIngressAdded runs when an ingress resource is added to the cluster.
func (c *CaddyController) onIngressAdded(obj *v1.Ingress) {
c.syncQueue.Add(IngressAddedAction{
resource: obj,
})
}
// onIngressUpdated is run when an ingress resource is updated in the cluster.
func (c *CaddyController) onIngressUpdated(old *v1.Ingress, new *v1.Ingress) {
c.syncQueue.Add(IngressUpdatedAction{
resource: new,
oldResource: old,
})
}
// onIngressDeleted is run when an ingress resource is deleted from the cluster.
func (c *CaddyController) onIngressDeleted(obj *v1.Ingress) {
c.syncQueue.Add(IngressDeletedAction{
resource: obj,
})
}
func (r IngressAddedAction) handle(c *CaddyController) error {
c.logger.Infof("Ingress created (%s/%s)", r.resource.Namespace, r.resource.Name)
// add this ingress to the internal store
c.resourceStore.AddIngress(r.resource)
// Ingress may now have a TLS config
return c.watchTLSSecrets()
}
func (r IngressUpdatedAction) handle(c *CaddyController) error {
c.logger.Infof("Ingress updated (%s/%s)", r.resource.Namespace, r.resource.Name)
// add or update this ingress in the internal store
c.resourceStore.AddIngress(r.resource)
// Ingress may now have a TLS config
return c.watchTLSSecrets()
}
func (r IngressDeletedAction) handle(c *CaddyController) error {
c.logger.Infof("Ingress deleted (%s/%s)", r.resource.Namespace, r.resource.Name)
// delete all resources from caddy config that are associated with this resource
c.resourceStore.PluckIngress(r.resource)
return nil
}

View File

@ -0,0 +1,139 @@
package controller
import (
"net"
"sort"
"strings"
"github.com/caddyserver/ingress/internal/k8s"
"go.uber.org/zap"
"gopkg.in/go-playground/pool.v3"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/client-go/kubernetes"
)
// dispatchSync is run every syncInterval duration to sync ingress source address fields.
func (c *CaddyController) dispatchSync() {
c.syncQueue.Add(SyncStatusAction{})
}
// SyncStatusAction provides an implementation of the action interface.
type SyncStatusAction struct {
}
// handle is run when a syncStatusAction appears in the queue.
func (r SyncStatusAction) handle(c *CaddyController) error {
return c.syncStatus(c.resourceStore.Ingresses)
}
// syncStatus ensures that the ingress source address points to this ingress controller's IP address.
func (c *CaddyController) syncStatus(ings []*networkingv1.Ingress) error {
addrs, err := k8s.GetAddresses(c.resourceStore.CurrentPod, c.kubeClient)
if err != nil {
return err
}
c.logger.Debugf("Syncing %d Ingress resources source addresses", len(ings))
c.updateIngStatuses(sliceToLoadBalancerIngress(addrs), ings)
return nil
}
// updateIngStatuses starts a queue and adds all monitored ingresses to update their status source address to the on
// that the ingress controller is running on. This is called by the syncStatus queue.
func (c *CaddyController) updateIngStatuses(controllerAddresses []networkingv1.IngressLoadBalancerIngress, ings []*networkingv1.Ingress) {
p := pool.NewLimited(10)
defer p.Close()
batch := p.Batch()
sort.SliceStable(controllerAddresses, lessLoadBalancerIngress(controllerAddresses))
for _, ing := range ings {
curIPs := ing.Status.LoadBalancer.Ingress
sort.SliceStable(curIPs, lessLoadBalancerIngress(curIPs))
// check to see if ingresses source address does not match the ingress controller's.
if ingressSliceEqual(curIPs, controllerAddresses) {
c.logger.Debugf("skipping update of Ingress %v/%v (no change)", ing.Namespace, ing.Name)
continue
}
batch.Queue(runUpdate(c.logger, ing, controllerAddresses, c.kubeClient))
}
batch.QueueComplete()
batch.WaitAll()
}
// runUpdate updates the ingress status field.
func runUpdate(logger *zap.SugaredLogger, ing *networkingv1.Ingress, status []networkingv1.IngressLoadBalancerIngress, client *kubernetes.Clientset) pool.WorkFunc {
return func(wu pool.WorkUnit) (interface{}, error) {
if wu.IsCancelled() {
return nil, nil
}
updated, err := k8s.UpdateIngressStatus(client, ing, status)
if err != nil {
logger.Warnf("error updating ingress rule: %v", err)
} else {
logger.Debugf(
"updating Ingress %v/%v status from %v to %v",
ing.Namespace,
ing.Name,
ing.Status.LoadBalancer.Ingress,
updated.Status.LoadBalancer.Ingress,
)
}
return true, nil
}
}
// ingressSliceEqual determines if the ingress source matches the ingress controller's.
func ingressSliceEqual(lhs, rhs []networkingv1.IngressLoadBalancerIngress) bool {
if len(lhs) != len(rhs) {
return false
}
for i := range lhs {
if lhs[i].IP != rhs[i].IP {
return false
}
if lhs[i].Hostname != rhs[i].Hostname {
return false
}
}
return true
}
// lessLoadBalancerIngress is a sorting function for ingress hostnames.
func lessLoadBalancerIngress(addrs []networkingv1.IngressLoadBalancerIngress) func(int, int) bool {
return func(a, b int) bool {
switch strings.Compare(addrs[a].Hostname, addrs[b].Hostname) {
case -1:
return true
case 1:
return false
}
return addrs[a].IP < addrs[b].IP
}
}
// sliceToLoadBalancerIngress converts a slice of IP and/or hostnames to LoadBalancerIngress
func sliceToLoadBalancerIngress(endpoints []string) []networkingv1.IngressLoadBalancerIngress {
lbi := []networkingv1.IngressLoadBalancerIngress{}
for _, ep := range endpoints {
if net.ParseIP(ep) == nil {
lbi = append(lbi, networkingv1.IngressLoadBalancerIngress{Hostname: ep})
} else {
lbi = append(lbi, networkingv1.IngressLoadBalancerIngress{IP: ep})
}
}
sort.SliceStable(lbi, func(a, b int) bool {
return lbi[a].IP < lbi[b].IP
})
return lbi
}

View File

@ -0,0 +1,124 @@
package controller
import (
"os"
"path/filepath"
"github.com/caddyserver/ingress/internal/k8s"
apiv1 "k8s.io/api/core/v1"
)
var CertFolder = filepath.FromSlash("/etc/caddy/certs")
// SecretAddedAction provides an implementation of the action interface.
type SecretAddedAction struct {
resource *apiv1.Secret
}
// SecretUpdatedAction provides an implementation of the action interface.
type SecretUpdatedAction struct {
resource *apiv1.Secret
oldResource *apiv1.Secret
}
// SecretDeletedAction provides an implementation of the action interface.
type SecretDeletedAction struct {
resource *apiv1.Secret
}
// onSecretAdded runs when a TLS secret resource is added to the cluster.
func (c *CaddyController) onSecretAdded(obj *apiv1.Secret) {
if k8s.IsManagedTLSSecret(obj, c.resourceStore.Ingresses) {
c.syncQueue.Add(SecretAddedAction{
resource: obj,
})
}
}
// onSecretUpdated is run when a TLS secret resource is updated in the cluster.
func (c *CaddyController) onSecretUpdated(old *apiv1.Secret, new *apiv1.Secret) {
if k8s.IsManagedTLSSecret(new, c.resourceStore.Ingresses) {
c.syncQueue.Add(SecretUpdatedAction{
resource: new,
oldResource: old,
})
}
}
// onSecretDeleted is run when a TLS secret resource is deleted from the cluster.
func (c *CaddyController) onSecretDeleted(obj *apiv1.Secret) {
c.syncQueue.Add(SecretDeletedAction{
resource: obj,
})
}
// writeFile writes a secret to a .pem file on disk.
func writeFile(s *apiv1.Secret) error {
content := make([]byte, 0)
for _, cert := range s.Data {
content = append(content, cert...)
}
err := os.WriteFile(filepath.Join(CertFolder, s.Name+".pem"), content, 0644)
if err != nil {
return err
}
return nil
}
func (r SecretAddedAction) handle(c *CaddyController) error {
c.logger.Infof("TLS secret created (%s/%s)", r.resource.Namespace, r.resource.Name)
return writeFile(r.resource)
}
func (r SecretUpdatedAction) handle(c *CaddyController) error {
c.logger.Infof("TLS secret updated (%s/%s)", r.resource.Namespace, r.resource.Name)
return writeFile(r.resource)
}
func (r SecretDeletedAction) handle(c *CaddyController) error {
c.logger.Infof("TLS secret deleted (%s/%s)", r.resource.Namespace, r.resource.Name)
return os.Remove(filepath.Join(CertFolder, r.resource.Name+".pem"))
}
// watchTLSSecrets Start listening to TLS secrets if at least one ingress needs it.
// It will sync the CertFolder with TLS secrets
func (c *CaddyController) watchTLSSecrets() error {
if c.informers.TLSSecret == nil && c.resourceStore.HasManagedTLS() {
// Init informers
params := k8s.TLSSecretParams{
InformerFactory: c.factories.WatchedNamespace,
}
c.informers.TLSSecret = k8s.WatchTLSSecrets(params, k8s.TLSSecretHandlers{
AddFunc: c.onSecretAdded,
UpdateFunc: c.onSecretUpdated,
DeleteFunc: c.onSecretDeleted,
})
// Run it
go c.informers.TLSSecret.Run(c.stopChan)
// Sync secrets
secrets, err := k8s.ListTLSSecrets(params, c.resourceStore.Ingresses)
if err != nil {
return err
}
if _, err := os.Stat(CertFolder); os.IsNotExist(err) {
err = os.MkdirAll(CertFolder, 0755)
if err != nil {
return err
}
}
for _, secret := range secrets {
if err := writeFile(secret); err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,272 @@
package controller
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/ingress/internal/k8s"
"github.com/caddyserver/ingress/pkg/store"
"go.uber.org/zap"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
// load required caddy plugins
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/proxyprotocol"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
_ "github.com/caddyserver/caddy/v2/modules/caddytls"
_ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek"
_ "github.com/caddyserver/caddy/v2/modules/metrics"
_ "github.com/caddyserver/ingress/pkg/storage"
)
const (
// how often we should attempt to keep ingress resource's source address in sync
syncInterval = time.Second * 30
// how often we resync informers resources (besides receiving updates)
resourcesSyncInterval = time.Hour * 1
)
// Action is an interface for ingress actions.
type Action interface {
handle(c *CaddyController) error
}
// Informer defines the required SharedIndexInformers that interact with the API server.
type Informer struct {
Ingress cache.SharedIndexInformer
ConfigMap cache.SharedIndexInformer
TLSSecret cache.SharedIndexInformer
}
// InformerFactory contains shared informer factory
// We need to type of factory:
// - One used to watch resources in the Pod namespaces (caddy config, secrets...)
// - Another one for Ingress resources in the selected namespace
type InformerFactory struct {
PodNamespace informers.SharedInformerFactory
WatchedNamespace informers.SharedInformerFactory
}
type Converter interface {
ConvertToCaddyConfig(store *store.Store) (interface{}, error)
}
// CaddyController represents a caddy ingress controller.
type CaddyController struct {
resourceStore *store.Store
kubeClient *kubernetes.Clientset
logger *zap.SugaredLogger
// main queue syncing ingresses, configmaps, ... with caddy
syncQueue workqueue.RateLimitingInterface
// informer factories
factories *InformerFactory
// informer contains the cache Informers
informers *Informer
// save last applied caddy config
lastAppliedConfig []byte
converter Converter
stopChan chan struct{}
}
func NewCaddyController(
logger *zap.SugaredLogger,
kubeClient *kubernetes.Clientset,
opts store.Options,
converter Converter,
stopChan chan struct{},
) *CaddyController {
controller := &CaddyController{
logger: logger,
kubeClient: kubeClient,
converter: converter,
stopChan: stopChan,
syncQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
informers: &Informer{},
factories: &InformerFactory{},
}
podInfo, err := k8s.GetPodDetails(kubeClient)
if err != nil {
logger.Fatalf("Unexpected error obtaining pod information: %v", err)
}
// Create informer factories
controller.factories.PodNamespace = informers.NewSharedInformerFactoryWithOptions(
kubeClient,
resourcesSyncInterval,
informers.WithNamespace(podInfo.Namespace),
)
controller.factories.WatchedNamespace = informers.NewSharedInformerFactoryWithOptions(
kubeClient,
resourcesSyncInterval,
informers.WithNamespace(opts.WatchNamespace),
)
// Watch ingress resources in selected namespaces
ingressParams := k8s.IngressParams{
InformerFactory: controller.factories.WatchedNamespace,
ClassName: opts.ClassName,
ClassNameRequired: opts.ClassNameRequired,
}
controller.informers.Ingress = k8s.WatchIngresses(ingressParams, k8s.IngressHandlers{
AddFunc: controller.onIngressAdded,
UpdateFunc: controller.onIngressUpdated,
DeleteFunc: controller.onIngressDeleted,
})
// Watch Configmap in the pod's namespace for global options
cmOptionsParams := k8s.ConfigMapParams{
Namespace: podInfo.Namespace,
InformerFactory: controller.factories.PodNamespace,
ConfigMapName: opts.ConfigMapName,
}
controller.informers.ConfigMap = k8s.WatchConfigMaps(cmOptionsParams, k8s.ConfigMapHandlers{
AddFunc: controller.onConfigMapAdded,
UpdateFunc: controller.onConfigMapUpdated,
DeleteFunc: controller.onConfigMapDeleted,
})
// Create resource store
controller.resourceStore = store.NewStore(opts, podInfo)
return controller
}
// Shutdown stops the caddy controller.
func (c *CaddyController) Shutdown() error {
// remove this ingress controller's ip from ingress resources.
c.updateIngStatuses([]networkingv1.IngressLoadBalancerIngress{{}}, c.resourceStore.Ingresses)
if err := caddy.Stop(); err != nil {
c.logger.Error("failed to stop caddy server", zap.Error(err))
return err
}
certmagic.CleanUpOwnLocks(context.TODO(), c.logger.Desugar())
return nil
}
// Run method starts the ingress controller.
func (c *CaddyController) Run() {
defer runtime.HandleCrash()
defer c.syncQueue.ShutDown()
// start informers where we listen to new / updated resources
go c.informers.ConfigMap.Run(c.stopChan)
go c.informers.Ingress.Run(c.stopChan)
// wait for all involved caches to be synced before processing items
// from the queue
if !cache.WaitForCacheSync(c.stopChan,
c.informers.ConfigMap.HasSynced,
c.informers.Ingress.HasSynced,
) {
runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
}
// start processing events for syncing ingress resources
go wait.Until(c.runWorker, time.Second, c.stopChan)
// start ingress status syncher and run every syncInterval
go wait.Until(c.dispatchSync, syncInterval, c.stopChan)
// wait for SIGTERM
<-c.stopChan
c.logger.Info("stopping ingress controller")
var exitCode int
err := c.Shutdown()
if err != nil {
c.logger.Error("could not shutdown ingress controller properly, " + err.Error())
exitCode = 1
}
os.Exit(exitCode)
}
// runWorker processes items in the event queue.
func (c *CaddyController) runWorker() {
for c.processNextItem() {
}
}
// processNextItem determines if there is an ingress item in the event queue and processes it.
func (c *CaddyController) processNextItem() bool {
// Wait until there is a new item in the working queue
action, quit := c.syncQueue.Get()
if quit {
return false
}
// Tell the queue that we are done with processing this key. This unblocks the key for other workers
// This allows safe parallel processing because two ingresses with the same key are never processed in
// parallel.
defer c.syncQueue.Done(action)
// Invoke the method containing the business logic
err := action.(Action).handle(c)
if err != nil {
c.handleErr(err, action)
return true
}
err = c.reloadCaddy()
if err != nil {
c.logger.Error("could not reload caddy: " + err.Error())
return true
}
return true
}
// handleErrs reports errors received from queue actions.
//
//goland:noinspection GoUnusedParameter
func (c *CaddyController) handleErr(err error, action interface{}) {
c.logger.Error(err.Error())
}
// reloadCaddy generate a caddy config from controller's store
func (c *CaddyController) reloadCaddy() error {
config, err := c.converter.ConvertToCaddyConfig(c.resourceStore)
if err != nil {
return err
}
j, err := json.Marshal(config)
if err != nil {
return err
}
if bytes.Equal(c.lastAppliedConfig, j) {
c.logger.Debug("caddy config did not change, skipping reload")
return nil
}
c.logger.Debug("reloading caddy with config", string(j))
err = caddy.Load(j, false)
if err != nil {
return fmt.Errorf("could not reload caddy config %v", err.Error())
}
c.lastAppliedConfig = j
return nil
}

View File

@ -0,0 +1,54 @@
package k8s
import (
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
type ConfigMapHandlers struct {
AddFunc func(obj *v1.ConfigMap)
UpdateFunc func(oldObj, newObj *v1.ConfigMap)
DeleteFunc func(obj *v1.ConfigMap)
}
type ConfigMapParams struct {
Namespace string
InformerFactory informers.SharedInformerFactory
ConfigMapName string
}
func isControllerConfigMap(cm *v1.ConfigMap, name string) bool {
return cm.GetName() == name
}
func WatchConfigMaps(options ConfigMapParams, funcs ConfigMapHandlers) cache.SharedIndexInformer {
informer := options.InformerFactory.Core().V1().ConfigMaps().Informer()
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
cm, ok := obj.(*v1.ConfigMap)
if ok && isControllerConfigMap(cm, options.ConfigMapName) {
funcs.AddFunc(cm)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldCM, ok1 := oldObj.(*v1.ConfigMap)
newCM, ok2 := newObj.(*v1.ConfigMap)
if ok1 && ok2 && isControllerConfigMap(newCM, options.ConfigMapName) {
funcs.UpdateFunc(oldCM, newCM)
}
},
DeleteFunc: func(obj interface{}) {
cm, ok := obj.(*v1.ConfigMap)
if ok && isControllerConfigMap(cm, options.ConfigMapName) {
funcs.DeleteFunc(cm)
}
},
})
return informer
}

View File

@ -0,0 +1,82 @@
package k8s
import (
"context"
"fmt"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
type IngressHandlers struct {
AddFunc func(obj *networkingv1.Ingress)
UpdateFunc func(oldObj, newObj *networkingv1.Ingress)
DeleteFunc func(obj *networkingv1.Ingress)
}
type IngressParams struct {
InformerFactory informers.SharedInformerFactory
ClassName string
ClassNameRequired bool
}
func WatchIngresses(options IngressParams, funcs IngressHandlers) cache.SharedIndexInformer {
informer := options.InformerFactory.Networking().V1().Ingresses().Informer()
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
ingress, ok := obj.(*networkingv1.Ingress)
if ok && isControllerIngress(options, ingress) {
funcs.AddFunc(ingress)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldIng, ok1 := oldObj.(*networkingv1.Ingress)
newIng, ok2 := newObj.(*networkingv1.Ingress)
if ok1 && ok2 && isControllerIngress(options, newIng) {
funcs.UpdateFunc(oldIng, newIng)
}
},
DeleteFunc: func(obj interface{}) {
ingress, ok := obj.(*networkingv1.Ingress)
if ok && isControllerIngress(options, ingress) {
funcs.DeleteFunc(ingress)
}
},
})
return informer
}
// isControllerIngress check if the ingress object can be controlled by us
func isControllerIngress(options IngressParams, ingress *networkingv1.Ingress) bool {
ingressClass := ingress.Annotations["kubernetes.io/ingress.class"]
if ingressClass == "" && ingress.Spec.IngressClassName != nil {
ingressClass = *ingress.Spec.IngressClassName
}
if !options.ClassNameRequired && ingressClass == "" {
return true
}
return ingressClass == options.ClassName
}
func UpdateIngressStatus(kubeClient *kubernetes.Clientset, ing *networkingv1.Ingress, status []networkingv1.IngressLoadBalancerIngress) (*networkingv1.Ingress, error) {
ingClient := kubeClient.NetworkingV1().Ingresses(ing.Namespace)
currIng, err := ingClient.Get(context.TODO(), ing.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("unexpected error searching Ingress %v/%v: %w", ing.Namespace, ing.Name, err)
}
currIng.Status.LoadBalancer.Ingress = status
return ingClient.UpdateStatus(context.TODO(), currIng, metav1.UpdateOptions{})
}

View File

@ -0,0 +1,99 @@
package k8s
import (
"context"
"fmt"
"os"
"github.com/caddyserver/ingress/pkg/store"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)
// GetAddresses gets the ip address or name of the node in the cluster that the
// ingress controller is running on.
func GetAddresses(p *store.PodInfo, kubeClient *kubernetes.Clientset) ([]string, error) {
var addrs []string
// Get services that may select this pod
svcs, err := kubeClient.CoreV1().Services(p.Namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, svc := range svcs.Items {
if isSubset(svc.Spec.Selector, p.Labels) {
addr := GetAddressFromService(&svc)
if addr != "" {
addrs = append(addrs, addr)
}
}
}
return addrs, nil
}
// Copied from https://github.com/kubernetes/kubernetes/pull/95179
func isSubset(subSet, superSet labels.Set) bool {
if len(superSet) == 0 {
return true
}
for k, v := range subSet {
value, ok := superSet[k]
if !ok {
return false
}
if value != v {
return false
}
}
return true
}
// GetAddressFromService returns the IP address or the name of a node in the cluster
func GetAddressFromService(service *apiv1.Service) string {
switch service.Spec.Type {
case apiv1.ServiceTypeNodePort:
case apiv1.ServiceTypeClusterIP:
if service.Spec.ClusterIP != apiv1.ClusterIPNone {
return service.Spec.ClusterIP
}
case apiv1.ServiceTypeExternalName:
return service.Spec.ExternalName
case apiv1.ServiceTypeLoadBalancer:
if len(service.Status.LoadBalancer.Ingress) > 0 {
ingress := service.Status.LoadBalancer.Ingress[0]
if ingress.Hostname != "" {
return ingress.Hostname
}
return ingress.IP
}
}
return ""
}
// GetPodDetails returns runtime information about the pod:
// name, namespace and IP of the node where it is running
func GetPodDetails(kubeClient *kubernetes.Clientset) (*store.PodInfo, error) {
podName := os.Getenv("POD_NAME")
podNs := os.Getenv("POD_NAMESPACE")
if podName == "" || podNs == "" {
return nil, fmt.Errorf("unable to get POD information (missing POD_NAME or POD_NAMESPACE environment variable")
}
pod, _ := kubeClient.CoreV1().Pods(podNs).Get(context.TODO(), podName, metav1.GetOptions{})
if pod == nil {
return nil, fmt.Errorf("unable to get POD information")
}
return &store.PodInfo{
Name: podName,
Namespace: podNs,
Labels: pod.GetLabels(),
}, nil
}

View File

@ -0,0 +1,76 @@
package k8s
import (
v12 "k8s.io/api/core/v1"
v1 "k8s.io/api/networking/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
type TLSSecretHandlers struct {
AddFunc func(obj *v12.Secret)
UpdateFunc func(oldObj, newObj *v12.Secret)
DeleteFunc func(obj *v12.Secret)
}
type TLSSecretParams struct {
InformerFactory informers.SharedInformerFactory
}
func WatchTLSSecrets(options TLSSecretParams, funcs TLSSecretHandlers) cache.SharedIndexInformer {
informer := options.InformerFactory.Core().V1().Secrets().Informer()
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
secret, ok := obj.(*v12.Secret)
if ok && secret.Type == v12.SecretTypeTLS {
funcs.AddFunc(secret)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldSecret, ok1 := oldObj.(*v12.Secret)
newSecret, ok2 := newObj.(*v12.Secret)
if ok1 && ok2 && newSecret.Type == v12.SecretTypeTLS {
funcs.UpdateFunc(oldSecret, newSecret)
}
},
DeleteFunc: func(obj interface{}) {
secret, ok := obj.(*v12.Secret)
if ok && secret.Type == v12.SecretTypeTLS {
funcs.DeleteFunc(secret)
}
},
})
return informer
}
func ListTLSSecrets(options TLSSecretParams, ings []*v1.Ingress) ([]*v12.Secret, error) {
lister := options.InformerFactory.Core().V1().Secrets().Lister()
tlsSecrets := []*v12.Secret{}
for _, ing := range ings {
for _, tlsRule := range ing.Spec.TLS {
secret, err := lister.Secrets(ing.Namespace).Get(tlsRule.SecretName)
// TODO Handle errors
if err == nil {
tlsSecrets = append(tlsSecrets, secret)
}
}
}
return tlsSecrets, nil
}
func IsManagedTLSSecret(secret *v12.Secret, ings []*v1.Ingress) bool {
for _, ing := range ings {
for _, tlsRule := range ing.Spec.TLS {
if tlsRule.SecretName == secret.Name && ing.Namespace == secret.Namespace {
return true
}
}
}
return false
}

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: caddy-global-options
namespace: caddy-system
data:
acmeCA: internal
# email: test@example.com
# debug: "false"

View File

@ -0,0 +1,24 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: example1
labels:
app: example1
spec:
replicas: 1
selector:
matchLabels:
app: example1
template:
metadata:
labels:
app: example1
spec:
containers:
- name: httpecho
image: hashicorp/http-echo
args:
- "-listen=:8080"
- '-text="hello world 1"'
ports:
- containerPort: 8080

View File

@ -0,0 +1,24 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: example2
labels:
app: example2
spec:
replicas: 1
selector:
matchLabels:
app: example2
template:
metadata:
labels:
app: example2
spec:
containers:
- name: httpecho
image: hashicorp/http-echo
args:
- "-listen=:8080"
- '-text="hello world 2"'
ports:
- containerPort: 8080

View File

@ -0,0 +1,46 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
annotations:
kubernetes.io/ingress.class: caddy
spec:
rules:
- host: example1.kubernetes.localhost
http:
paths:
- path: /hello1
pathType: Prefix
backend:
service:
name: example1
port:
number: 8080
- path: /hello2
pathType: Prefix
backend:
service:
name: example2
port:
number: 8080
- host: example2.kubernetes.localhost
http:
paths:
- path: /hello1
pathType: Prefix
backend:
service:
name: example1
port:
number: 8080
- path: /hello2
pathType: Prefix
backend:
service:
name: example2
port:
number: 8080
# tls:
# - secretName: ssl-example2.kubernetes.localhost
# hosts:
# - example2.caddy.dev

View File

@ -0,0 +1,13 @@
kind: Service
apiVersion: v1
metadata:
name: example1
spec:
type: ClusterIP
selector:
app: example1
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080

View File

@ -0,0 +1,13 @@
kind: Service
apiVersion: v1
metadata:
name: example2
spec:
type: ClusterIP
selector:
app: example2
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080

View File

@ -0,0 +1,65 @@
package converter
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
// StorageValues represents the config for certmagic storage providers.
type StorageValues struct {
Namespace string `json:"namespace"`
LeaseId string `json:"leaseId"`
}
// Storage represents the certmagic storage configuration.
type Storage struct {
System string `json:"module"`
StorageValues
}
// Config represents a caddy2 config file.
type Config struct {
Admin caddy.AdminConfig `json:"admin,omitempty"`
Storage Storage `json:"storage"`
Apps map[string]interface{} `json:"apps"`
Logging caddy.Logging `json:"logging"`
}
func (c Config) GetHTTPServer() *caddyhttp.Server {
return c.Apps["http"].(*caddyhttp.App).Servers[HttpServer]
}
func (c Config) GetMetricsServer() *caddyhttp.Server {
return c.Apps["http"].(*caddyhttp.App).Servers[MetricsServer]
}
func (c Config) GetTLSApp() *caddytls.TLS {
return c.Apps["tls"].(*caddytls.TLS)
}
func NewConfig() *Config {
return &Config{
Logging: caddy.Logging{},
Apps: map[string]interface{}{
"tls": &caddytls.TLS{CertificatesRaw: caddy.ModuleMap{}},
"http": &caddyhttp.App{
Servers: map[string]*caddyhttp.Server{
HttpServer: {
AutoHTTPS: &caddyhttp.AutoHTTPSConfig{},
// Listen to both :80 and :443 ports in order
// to use the same listener wrappers (PROXY protocol use it)
Listen: []string{":80", ":443"},
TLSConnPolicies: caddytls.ConnectionPolicies{
&caddytls.ConnectionPolicy{},
},
},
MetricsServer: {
Listen: []string{":9765"},
AutoHTTPS: &caddyhttp.AutoHTTPSConfig{Disabled: true},
},
},
},
},
}
}

View File

@ -0,0 +1,113 @@
package converter
import (
"fmt"
"sort"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/store"
v1 "k8s.io/api/networking/v1"
)
const (
HttpServer = "ingress_server"
MetricsServer = "metrics_server"
)
// GlobalMiddleware is called with a default caddy config
// already configured with:
// - Secret storage store
// - A TLS App (https://caddyserver.com/docs/json/apps/tls/)
// - A HTTP App with an HTTP server listening to 80 443 ports (https://caddyserver.com/docs/json/apps/http/)
type GlobalMiddleware interface {
GlobalHandler(config *Config, store *store.Store) error
}
type IngressMiddlewareInput struct {
Config *Config
Store *store.Store
Ingress *v1.Ingress
Rule v1.IngressRule
Path v1.HTTPIngressPath
Route *caddyhttp.Route
}
// IngressMiddleware is called for each Caddy route that is generated for a specific
// ingress. It allows anyone to manipulate caddy routes before sending it to caddy.
type IngressMiddleware interface {
IngressHandler(input IngressMiddlewareInput) (*caddyhttp.Route, error)
}
type Plugin interface {
IngressPlugin() PluginInfo
}
type PluginInfo struct {
Name string
Priority int
New func() Plugin
}
func RegisterPlugin(m Plugin) {
plugin := m.IngressPlugin()
if _, ok := plugins[plugin.Name]; ok {
panic(fmt.Sprintf("plugin already registered: %s", plugin.Name))
}
plugins[plugin.Name] = plugin
pluginInstances[plugin.Name] = plugin.New()
}
func getOrderIndex(order []string, plugin string) int {
for idx, o := range order {
if plugin == o {
return idx
}
}
return -1
}
func sortPlugins(plugins []PluginInfo, order []string) []PluginInfo {
sort.SliceStable(plugins, func(i, j int) bool {
iPlugin, jPlugin := plugins[i], plugins[j]
iSortedIdx := getOrderIndex(order, iPlugin.Name)
jSortedIdx := getOrderIndex(order, jPlugin.Name)
if iSortedIdx != jSortedIdx {
return iSortedIdx > jSortedIdx
}
if iPlugin.Priority != jPlugin.Priority {
return iPlugin.Priority > jPlugin.Priority
}
return iPlugin.Name < jPlugin.Name
})
return plugins
}
// Plugins return a sorted array of plugin instances.
// Sort is made following these rules:
// - Plugins specified in the order slice will always go first (in the order specified in the slice)
// - A Plugin with higher priority will go before a plugin with lower priority
// - If 2 plugins have the same priority (and not in order slice), they will be sorted by plugin name
func Plugins(order []string) []Plugin {
sortedPlugins := make([]PluginInfo, 0, len(plugins))
for _, p := range plugins {
sortedPlugins = append(sortedPlugins, p)
}
sortPlugins(sortedPlugins, order)
pluginArr := make([]Plugin, 0, len(plugins))
for _, p := range sortedPlugins {
pluginArr = append(pluginArr, pluginInstances[p.Name])
}
return pluginArr
}
var (
plugins = make(map[string]PluginInfo)
pluginInstances = make(map[string]Plugin)
)

View File

@ -0,0 +1,53 @@
package converter
import "testing"
func TestSortPlugins(t *testing.T) {
tests := []struct {
name string
order []string
plugins []PluginInfo
expect []string
}{
{
name: "default to alpha sort",
order: nil,
plugins: []PluginInfo{{Name: "b"}, {Name: "c"}, {Name: "a"}},
expect: []string{"a", "b", "c"},
},
{
name: "use priority when specified",
order: nil,
plugins: []PluginInfo{{Name: "b"}, {Name: "a", Priority: 20}, {Name: "c", Priority: 10}},
expect: []string{"a", "c", "b"},
},
{
name: "fallback to alpha when no priority",
order: nil,
plugins: []PluginInfo{{Name: "b"}, {Name: "a"}, {Name: "c", Priority: 20}},
expect: []string{"c", "a", "b"},
},
{
name: "specify order",
order: []string{"c"},
plugins: []PluginInfo{{Name: "b"}, {Name: "a"}, {Name: "c"}},
expect: []string{"c", "a", "b"},
},
{
name: "order overrides other settings",
order: []string{"c"},
plugins: []PluginInfo{{Name: "b", Priority: 10}, {Name: "a"}, {Name: "c"}},
expect: []string{"c", "b", "a"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sortPlugins(test.plugins, test.order)
for i, plugin := range test.plugins {
if test.expect[i] != plugin.Name {
t.Errorf("expected order to match %v: got %v, expected %v", test.expect, plugin.Name, test.expect[i])
}
}
})
}
}

View File

@ -0,0 +1,46 @@
package proxy
import (
"net"
"github.com/caddyserver/caddy/v2"
"github.com/pires/go-proxyproto"
)
var (
_ = caddy.Provisioner(&Wrapper{})
_ = caddy.Module(&Wrapper{})
_ = caddy.ListenerWrapper(&Wrapper{})
)
func init() {
caddy.RegisterModule(Wrapper{})
}
// Wrapper provides PROXY protocol support to Caddy by implementing the caddy.ListenerWrapper interface.
// It must be loaded before the `tls` listener.
//
// Deprecated: This caddy module should be replaced by the included proxy_protocol listener in Caddy.
type Wrapper struct {
policy proxyproto.PolicyFunc
}
func (Wrapper) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.listeners.proxy_protocol",
New: func() caddy.Module { return new(Wrapper) },
}
}
func (pp *Wrapper) Provision(ctx caddy.Context) error {
pp.policy = func(upstream net.Addr) (proxyproto.Policy, error) {
return proxyproto.REQUIRE, nil
}
return nil
}
func (pp *Wrapper) WrapListener(l net.Listener) net.Listener {
pL := &proxyproto.Listener{Listener: l, Policy: pp.policy}
return pL
}

View File

@ -0,0 +1,290 @@
package storage
import (
"context"
"fmt"
"io/fs"
"regexp"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
"github.com/google/uuid"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/leaderelection/resourcelock"
)
const (
leaseDuration = 5 * time.Second
leaseRenewInterval = 2 * time.Second
leasePollInterval = 5 * time.Second
leasePrefix = "caddy-lock-"
keyPrefix = "caddy.ingress--"
)
// matchLabels are attached to each resource so that they can be found in the future.
var matchLabels = map[string]string{
"manager": "caddy",
}
// specialChars is a regex that matches all special characters except '-'.
var specialChars = regexp.MustCompile("[^\\da-zA-Z-]+")
// cleanKey strips all special characters that are not supported by kubernetes names and converts them to a '.'.
// sequences like '.*.' are also converted to a single '.'.
func cleanKey(key string, prefix string) string {
return prefix + specialChars.ReplaceAllString(key, ".")
}
// SecretStorage facilitates storing certificates retrieved by certmagic in kubernetes secrets.
type SecretStorage struct {
Namespace string
LeaseId string
kubeClient *kubernetes.Clientset
logger *zap.Logger
}
func init() {
caddy.RegisterModule(SecretStorage{})
}
func (SecretStorage) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.storage.secret_store",
New: func() caddy.Module { return new(SecretStorage) },
}
}
// Provisions the SecretStorage instance.
func (s *SecretStorage) Provision(ctx caddy.Context) error {
config, _ := clientcmd.BuildConfigFromFlags("", "")
// creates the clientset
clientset, _ := kubernetes.NewForConfig(config)
s.logger = ctx.Logger(s)
s.kubeClient = clientset
if s.LeaseId == "" {
s.LeaseId = uuid.New().String()
}
return nil
}
// CertMagicStorage returns a certmagic storage type to be used by caddy.
func (s *SecretStorage) CertMagicStorage() (certmagic.Storage, error) {
return s, nil
}
// Exists returns true if key exists in fs.
func (s *SecretStorage) Exists(ctx context.Context, key string) bool {
s.logger.Debug("finding secret", zap.String("name", key))
secrets, err := s.kubeClient.CoreV1().Secrets(s.Namespace).List(context.TODO(), metav1.ListOptions{
FieldSelector: fmt.Sprintf("metadata.name=%v", cleanKey(key, keyPrefix)),
})
if err != nil {
return false
}
var found bool
for _, i := range secrets.Items {
if i.ObjectMeta.Name == cleanKey(key, keyPrefix) {
found = true
break
}
}
return found
}
// Store saves value at key. More than certs and keys are stored by certmagic in secrets.
func (s *SecretStorage) Store(ctx context.Context, key string, value []byte) error {
se := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: cleanKey(key, keyPrefix),
Labels: matchLabels,
},
Data: map[string][]byte{
"value": value,
},
}
var err error
if s.Exists(ctx, key) {
s.logger.Debug("creating secret", zap.String("name", key))
_, err = s.kubeClient.CoreV1().Secrets(s.Namespace).Update(context.TODO(), &se, metav1.UpdateOptions{})
} else {
s.logger.Debug("updating secret", zap.String("name", key))
_, err = s.kubeClient.CoreV1().Secrets(s.Namespace).Create(context.TODO(), &se, metav1.CreateOptions{})
}
if err != nil {
return err
}
return nil
}
// Load retrieves the value at the given key.
func (s *SecretStorage) Load(ctx context.Context, key string) ([]byte, error) {
secret, err := s.kubeClient.CoreV1().Secrets(s.Namespace).Get(context.TODO(), cleanKey(key, keyPrefix), metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
return nil, fs.ErrNotExist
}
return nil, err
}
s.logger.Debug("loading secret", zap.String("name", key))
return secret.Data["value"], nil
}
// Delete deletes the value at the given key.
func (s *SecretStorage) Delete(ctx context.Context, key string) error {
err := s.kubeClient.CoreV1().Secrets(s.Namespace).Delete(context.TODO(), cleanKey(key, keyPrefix), metav1.DeleteOptions{})
if err != nil {
return err
}
s.logger.Debug("deleting secret", zap.String("name", key))
return nil
}
// List returns all keys that match prefix.
func (s *SecretStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) {
var keys []string
s.logger.Debug("listing secrets", zap.String("name", prefix))
secrets, err := s.kubeClient.CoreV1().Secrets(s.Namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labels.SelectorFromSet(matchLabels).String(),
})
if err != nil {
return keys, err
}
// TODO :- do we need to handle the recursive flag?
for _, secret := range secrets.Items {
key := secret.ObjectMeta.Name
if strings.HasPrefix(key, cleanKey(prefix, keyPrefix)) {
keys = append(keys, strings.TrimPrefix(key, keyPrefix))
}
}
return keys, err
}
// Stat returns information about key.
func (s *SecretStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) {
secret, err := s.kubeClient.CoreV1().Secrets(s.Namespace).Get(context.TODO(), cleanKey(key, keyPrefix), metav1.GetOptions{})
if err != nil {
return certmagic.KeyInfo{}, err
}
s.logger.Debug("stats secret", zap.String("name", key))
return certmagic.KeyInfo{
Key: key,
Modified: secret.GetCreationTimestamp().UTC(),
Size: int64(len(secret.Data["value"])),
IsTerminal: false,
}, nil
}
func (s *SecretStorage) Lock(ctx context.Context, key string) error {
for {
_, err := s.tryAcquireOrRenew(ctx, cleanKey(key, leasePrefix), false)
if err == nil {
go s.keepLockUpdated(ctx, cleanKey(key, leasePrefix))
return nil
}
select {
case <-time.After(leasePollInterval):
case <-ctx.Done():
return ctx.Err()
}
}
}
func (s *SecretStorage) keepLockUpdated(ctx context.Context, key string) {
for {
time.Sleep(leaseRenewInterval)
done, err := s.tryAcquireOrRenew(ctx, key, true)
if err != nil {
return
}
if done {
return
}
}
}
func (s *SecretStorage) tryAcquireOrRenew(ctx context.Context, key string, shouldExist bool) (bool, error) {
now := metav1.Now()
lock := resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: key,
Namespace: s.Namespace,
},
Client: s.kubeClient.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: s.LeaseId,
},
}
ler := resourcelock.LeaderElectionRecord{
HolderIdentity: lock.Identity(),
LeaseDurationSeconds: 5,
AcquireTime: now,
RenewTime: now,
}
currLer, _, err := lock.Get(ctx)
// 1. obtain or create the ElectionRecord
if err != nil {
if !errors.IsNotFound(err) {
return true, err
}
if shouldExist {
return true, nil // Lock has been released
}
if err = lock.Create(ctx, ler); err != nil {
return true, err
}
return false, nil
}
// 2. Record obtained, check the Identity & Time
if currLer.HolderIdentity != "" &&
currLer.RenewTime.Add(leaseDuration).After(now.Time) &&
currLer.HolderIdentity != lock.Identity() {
return true, fmt.Errorf("lock is held by %v and has not yet expired", currLer.HolderIdentity)
}
// 3. We're going to try to update the existing one
if currLer.HolderIdentity == lock.Identity() {
ler.AcquireTime = currLer.AcquireTime
ler.LeaderTransitions = currLer.LeaderTransitions
} else {
ler.LeaderTransitions = currLer.LeaderTransitions + 1
}
if err = lock.Update(ctx, ler); err != nil {
return true, fmt.Errorf("failed to update lock: %v", err)
}
return false, nil
}
func (s *SecretStorage) Unlock(ctx context.Context, key string) error {
err := s.kubeClient.CoordinationV1().Leases(s.Namespace).Delete(context.TODO(), cleanKey(key, leasePrefix), metav1.DeleteOptions{})
return err
}

View File

@ -0,0 +1,63 @@
package store
import (
"fmt"
"reflect"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/mitchellh/mapstructure"
apiv1 "k8s.io/api/core/v1"
)
// ConfigMapOptions represents global options set through a configmap
type ConfigMapOptions struct {
Debug bool `json:"debug,omitempty"`
AcmeCA string `json:"acmeCA,omitempty"`
AcmeEABKeyId string `json:"acmeEABKeyId,omitempty"`
AcmeEABMacKey string `json:"acmeEABMacKey,omitempty"`
Email string `json:"email,omitempty"`
ExperimentalSmartSort bool `json:"experimentalSmartSort,omitempty"`
ProxyProtocol bool `json:"proxyProtocol,omitempty"`
Metrics bool `json:"metrics,omitempty"`
OnDemandTLS bool `json:"onDemandTLS,omitempty"`
OnDemandAsk string `json:"onDemandAsk,omitempty"`
OCSPCheckInterval caddy.Duration `json:"ocspCheckInterval,omitempty"`
}
func stringToCaddyDurationHookFunc() mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(caddy.Duration(time.Second)) {
return data, nil
}
return caddy.ParseDuration(data.(string))
}
}
func ParseConfigMap(cm *apiv1.ConfigMap) (*ConfigMapOptions, error) {
// parse configmap
cfgMap := ConfigMapOptions{}
config := &mapstructure.DecoderConfig{
Metadata: nil,
WeaklyTypedInput: true,
Result: &cfgMap,
TagName: "json",
DecodeHook: mapstructure.ComposeDecodeHookFunc(
stringToCaddyDurationHookFunc(),
),
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return nil, fmt.Errorf("unexpected error creating decoder: %w", err)
}
err = decoder.Decode(cm.Data)
if err != nil {
return nil, fmt.Errorf("unexpected error parsing configmap: %w", err)
}
return &cfgMap, nil
}

View File

@ -0,0 +1,12 @@
package store
// Options represents ingress controller config received through cli arguments.
type Options struct {
WatchNamespace string
ConfigMapName string
ClassName string
ClassNameRequired bool
Verbose bool
LeaseId string
PluginsOrder []string
}

View File

@ -0,0 +1,10 @@
package store
// PodInfo contains runtime information about the pod running the Ingress controller
type PodInfo struct {
Name string
Namespace string
// Labels selectors of the running pod
// This is used to search for other Ingress controller pods
Labels map[string]string
}

View File

@ -0,0 +1,72 @@
package store
import (
v1 "k8s.io/api/networking/v1"
)
// Store contains resources used to generate Caddy config
type Store struct {
Options *Options
ConfigMap *ConfigMapOptions
Ingresses []*v1.Ingress
CurrentPod *PodInfo
}
// NewStore returns a new store that keeps track of K8S resources needed by the controller.
func NewStore(opts Options, podInfo *PodInfo) *Store {
s := &Store{
Options: &opts,
Ingresses: []*v1.Ingress{},
ConfigMap: &ConfigMapOptions{},
CurrentPod: podInfo,
}
return s
}
// AddIngress adds an ingress to the store. It updates the element at the given index if it is unique.
func (s *Store) AddIngress(ing *v1.Ingress) {
isUniq := true
for i := range s.Ingresses {
in := s.Ingresses[i]
if in.GetUID() == ing.GetUID() {
isUniq = false
s.Ingresses[i] = ing
}
}
if isUniq {
s.Ingresses = append(s.Ingresses, ing)
}
}
// PluckIngress removes the ingress passed in as an argument from the stores list of ingresses.
func (s *Store) PluckIngress(ing *v1.Ingress) {
id := ing.GetUID()
var index int
var hasMatch bool
for i := range s.Ingresses {
if s.Ingresses[i].GetUID() == id {
index = i
hasMatch = true
break
}
}
// since order is not important we can swap the element to delete with the one at the end of the slice
// and then set ingresses to the n-1 first elements
if hasMatch {
s.Ingresses[len(s.Ingresses)-1], s.Ingresses[index] = s.Ingresses[index], s.Ingresses[len(s.Ingresses)-1]
s.Ingresses = s.Ingresses[:len(s.Ingresses)-1]
}
}
func (s *Store) HasManagedTLS() bool {
for _, ing := range s.Ingresses {
if len(ing.Spec.TLS) > 0 {
return true
}
}
return false
}

View File

@ -0,0 +1,200 @@
package store
import (
"testing"
v1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
typev1 "k8s.io/apimachinery/pkg/types"
)
func TestStoreIngresses(t *testing.T) {
tests := []struct {
name string
addIngresses []string
removeIngresses []string
expectCount int
}{
{
name: "No ingress added nor removed",
addIngresses: []string{},
removeIngresses: []string{},
expectCount: 0,
},
{
name: "One ingress added, no ingress removed",
addIngresses: []string{"first"},
removeIngresses: []string{},
expectCount: 1,
},
{
name: "Two ingresses added, no ingress removed",
addIngresses: []string{"first", "second"},
removeIngresses: []string{},
expectCount: 2,
},
{
name: "Two ingresses added, first ingress removed",
addIngresses: []string{"first", "second"},
removeIngresses: []string{"first"},
expectCount: 1,
},
{
name: "Two ingresses added, second ingress removed",
addIngresses: []string{"first", "second"},
removeIngresses: []string{"second"},
expectCount: 1,
},
{
name: "Two ingresses added, both ingresses removed",
addIngresses: []string{"first", "second"},
removeIngresses: []string{"second", "first"},
expectCount: 0,
},
{
name: "Two ingresses added, one existing and one non existing ingress removed",
addIngresses: []string{"first", "second"},
removeIngresses: []string{"second", "third"},
expectCount: 1,
},
{
name: "Remove non existing ingresses",
addIngresses: []string{"first", "second"},
removeIngresses: []string{"third", "forth"},
expectCount: 2,
},
{
name: "Remove non existing ingresses from an empty list",
addIngresses: []string{},
removeIngresses: []string{"first", "second"},
expectCount: 0,
},
{
name: "Add the same ingress multiple time",
addIngresses: []string{"first", "first", "first"},
removeIngresses: []string{},
expectCount: 1,
},
{
name: "Remove the same ingress multiple time",
addIngresses: []string{"first", "second"},
removeIngresses: []string{"second", "second", "second"},
expectCount: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := NewStore(Options{}, &PodInfo{})
for _, uid := range test.addIngresses {
i := createIngress(uid)
s.AddIngress(&i)
}
for _, uid := range test.removeIngresses {
i := createIngress(uid)
s.PluckIngress(&i)
}
if test.expectCount != len(s.Ingresses) {
t.Errorf("Number of ingresses do not match expectation in %s: got %v, expected %v", test.name, len(s.Ingresses), test.expectCount)
}
})
}
}
func TestStoreReturnIfHasManagedTLS(t *testing.T) {
tests := []struct {
name string
ingresses []v1.Ingress
expect bool
}{
{
name: "No ingress",
ingresses: []v1.Ingress{},
expect: false,
},
{
name: "One ingress without certificate",
ingresses: []v1.Ingress{
createIngress("first"),
},
expect: false,
},
{
name: "One ingress with certificate",
ingresses: []v1.Ingress{
createIngressTLS("first", []string{"host1"}, "mysecret1"),
},
expect: true,
},
{
name: "Multiple ingresses without certificate",
ingresses: []v1.Ingress{
createIngress("first"),
createIngress("second"),
createIngress("third"),
},
expect: false,
},
{
name: "Three ingresses mixed with and without certificate",
ingresses: []v1.Ingress{
createIngressTLS("first", []string{"host1a", "host1b"}, "mysecret1"),
createIngress("second"),
createIngressTLS("third", []string{"host3"}, "mysecret3"),
},
expect: true,
},
{
name: "Ingress replaced without a certificate",
ingresses: []v1.Ingress{
createIngressTLS("first", []string{"host1a", "host1b"}, "mysecret1"),
createIngress("first"),
},
expect: false,
},
{
name: "Ingress replaced with a certificate",
ingresses: []v1.Ingress{
createIngress("first"),
createIngressTLS("first", []string{"host1a", "host1b"}, "mysecret1"),
},
expect: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := NewStore(Options{}, &PodInfo{})
for _, i := range test.ingresses {
s.AddIngress(&i)
}
if test.expect != s.HasManagedTLS() {
t.Errorf("managed TLS do not match expectation in %s: got %v, expected %v", test.name, s.HasManagedTLS(), test.expect)
}
})
}
}
func createIngressTLS(uid string, hosts []string, secret string) v1.Ingress {
i := createIngress(uid)
i.Spec = v1.IngressSpec{
TLS: []v1.IngressTLS{
{
Hosts: hosts,
SecretName: secret,
},
},
}
return i
}
func createIngress(uid string) v1.Ingress {
return v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
UID: typev1.UID(uid),
},
}
}

View File

@ -0,0 +1,37 @@
apiVersion: skaffold/v4beta7
kind: Config
metadata:
name: caddy-ingress-controller
build:
artifacts:
- image: caddy-ingress
context: .
docker:
dockerfile: Dockerfile.dev
deploy:
helm:
releases:
- name: caddy-ingress-development
namespace: caddy-system
chartPath: charts/caddy-ingress-controller
createNamespace: true
setValueTemplates:
image:
repository: "{{ .IMAGE_REPO_NO_DOMAIN_caddy_ingress }}"
tag: "{{.IMAGE_TAG_caddy_ingress}}@{{.IMAGE_DIGEST_caddy_ingress}}"
manifests:
rawYaml:
- ./kubernetes/sample/*.yaml
portForward:
- resourceType: service
resourceName: caddy-ingress-development-caddy-ingress-controller
namespace: caddy-system
address: 0.0.0.0
port: 80
localPort: 8080
- resourceType: service
resourceName: caddy-ingress-development-caddy-ingress-controller
namespace: caddy-system
address: 0.0.0.0
port: 443
localPort: 8443

View File

@ -0,0 +1,174 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: cryptpad
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cryptpad
namespace: cryptpad
spec:
selector:
matchLabels:
app: cryptpad
template:
metadata:
labels:
app: cryptpad
spec:
volumes:
- name: config
configMap:
name: config
- name: cryptpad
persistentVolumeClaim:
claimName: cryptpad
containers:
- name: cryptpad
image: quay.io/ffddorf/cryptpad:4.8.0
resources:
limits:
memory: "512Mi"
cpu: "500m"
ports:
- containerPort: 3000
volumeMounts:
- name: config
mountPath: /cryptpad/config
- name: cryptpad
mountPath: /cryptpad/data
---
apiVersion: v1
kind: ConfigMap
metadata:
name: config
namespace: cryptpad
data:
config.js: |
module.exports = {
adminKeys: [
"[nomaster@pad.freifunk-duesseldorf.de/WUdnwywXbKnT6QsT6OuZXQqJOQCZwiZDz2y3492oGpw=]",
],
adminEmail: 'kontakt@freifunk-duesseldorf.de',
allowSubscriptions: false,
archivePath: './data/archive',
blobPath: './data/blob',
blobStagingPath: './data/blobstage',
blockPath: './data/block',
filePath: './data/store',
httpAddress: '::',
httpPort: 3000,
httpSafeOrigin: 'https://cryptpad.freifunk-duesseldorf.de/',
httpUnsafeOrigin: 'https://pad.freifunk-duesseldorf.de/',
logFeedback: false,
logLevel: 'info',
logToStdout: true,
noSubscriptionButton: true,
pinPath: './data/pins',
removeDonateButton: true,
supportMailboxPublicKey: 'bLZQjf8j/kQnV3LLT64ROORvJjzJzz7FQRLWh1DV6B4=',
taskPath: './data/tasks',
verbose: false,
};
---
apiVersion: v1
kind: Service
metadata:
name: cryptpad
namespace: cryptpad
spec:
selector:
app: cryptpad
ports:
- port: 3000
targetPort: 3000
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: security
namespace: cryptpad
spec:
headers:
stsSeconds: 63072000
customResponseHeaders:
cross-origin-resource-policy: cross-origin
cross-origin-embedder-policy: require-corp
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: cryptpad
namespace: cryptpad
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
kubernetes.io/ingress.class: traefik
kubernetes.io/tls-acme: "true"
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.middlewares: cryptpad-security@kubernetescrd
spec:
tls:
- hosts:
- cryptpad.freifunk-duesseldorf.de
- pad.freifunk-duesseldorf.de
secretName: cryptpad-tls-prod
rules:
- host: cryptpad.freifunk-duesseldorf.de
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: cryptpad
port:
number: 3000
- host: pad.freifunk-duesseldorf.de
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: cryptpad
port:
number: 3000
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cryptpad
namespace: cryptpad
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
volumeName: cryptpad
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: cryptpad
spec:
capacity:
storage: 4Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-path
local:
path: /data/cryptpad/cryptpad
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k3s1

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cryptpad
namespace: cryptpad
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi
storageClassName: local-path

View File

@ -0,0 +1,25 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"regexManagers": [
{
"fileMatch": ["^version\\.txt$"],
"matchStrings": ["^(?<currentValue>\\d+\\.\\d+\\.\\d+)"],
"depNameTemplate": "xwiki-labs/cryptpad-docker",
"datasourceTemplate": "github-tags"
},
{
"fileMatch": ["^cryptpad\\.yml$"],
"matchStrings": [
"^\\s+image:\\s?quay\\.io/ffddorf/cryptpad:(?<currentValue>\\d+\\.\\d+\\.\\d+)$"
],
"depNameTemplate": "xwiki-labs/cryptpad-docker",
"datasourceTemplate": "github-tags"
}
],
"packageRules": [
{
"matchPackageNames": ["xwiki-labs/cryptpad"],
"groupName": "cryptpad"
}
]
}

View File

@ -0,0 +1 @@
5.0.0

View File

@ -0,0 +1,256 @@
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ tree
.
├── etherpad-lite-k8s
│   ├── configmap.yaml
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
├── etherpad-lite-k8s-kubedb-mysql
│   ├── configmap.yaml
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── name-prefix-transformer-config.yaml
├── kubedb-mysql-etherpad-lite
│   ├── etherpad-mysql.yaml
│   ├── kustomization.yaml
│   ├── README.md
│   └── transformer-config-kubedb.yaml
├── kubedb-mysql-etherpad-lite-with-init-script
│   ├── etherpad-mysql-init-configmap.yaml
│   ├── etherpad-mysql-with-init-script.yaml
│   └── kustomization.yaml
└── test-etherpad-lite-mysql-with-namePrefix
└── kustomization.yaml
6 directories, 16 files
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: etherpad
data:
settings.json: |
{
"skinName":"colibris",
"title":"Etherpad on Kubernetes"
}
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: etherpad
spec:
replicas: 1
selector:
matchLabels:
app: etherpad
template:
metadata:
labels:
app: etherpad
spec:
containers:
- name: etherpad
image: etherpad/etherpad:1.7.5
ports:
- containerPort: 9001
name: web
volumeMounts:
- name: "config"
mountPath: "/opt/etherpad/settings.json"
subPath: "settings.json"
volumes:
- name: config
configMap:
name: etherpad
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: etherpad
spec:
selector:
app: etherpad
ports:
- name: web
port: 80
targetPort: web
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s-kubedb-mysql/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: etherpad
data:
settings.json: |
{
"skinName":"colibris",
"title":"Etherpad on Kubernetes w/ MySQL",
"dbType": "${ETHERPAD_DB_TYPE:mysql}",
"dbSettings": {
"database": "${ETHERPAD_DB_DATABASE}",
"host": "${ETHERPAD_DB_HOST}",
"password": "${ETHERPAD_DB_PASSWORD}",
"user": "${ETHERPAD_DB_USER}"
}
}
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s-kubedb-mysql/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: etherpad
spec:
template:
spec:
containers:
- name: etherpad
env:
- name: ETHERPAD_DB_TYPE
value: mysql
- name: ETHERPAD_DB_HOST
value: $(MYSQL_SERVICE)
- name: ETHERPAD_DB_DATABASE
value: etherpad_lite_db
- name: ETHERPAD_DB_USER
valueFrom:
secretKeyRef:
name: etherpad-mysql-auth
key: username
- name: ETHERPAD_DB_PASSWORD
valueFrom:
secretKeyRef:
name: etherpad-mysql-auth
key: password
volumeMounts:
- name: "config"
mountPath: "/opt/etherpad-lite/settings.json"
subPath: "settings.json"
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s-kubedb-mysql/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../kubedb-mysql-etherpad-lite-with-init-script
- ../etherpad-lite-k8s
patchesStrategicMerge:
- configmap.yaml
- deployment.yaml
images:
- name: etherpad/etherpad
# This is required until etherpad-lite 1.8 comes out to be able to use env vars in settings.json
newTag: latest
configurations:
- name-prefix-transformer-config.yaml
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat etherpad-lite-k8s-kubedb-mysql/name-prefix-transformer-config.yaml
namePrefix:
- apiVersion: apps/v1
kind: Deployment
path: spec/template/spec/containers/env/valueFrom/secretKeyRef/name
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite/etherpad-mysql.yaml
apiVersion: kubedb.com/v1alpha1
kind: MySQL
metadata:
name: etherpad-mysql
spec:
version: "5.7.25"
storageType: Durable
terminationPolicy: WipeOut
storage:
storageClassName: "default"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- etherpad-mysql.yaml
vars:
- name: MYSQL_SERVICE
objref:
apiVersion: kubedb.com/v1alpha1
kind: MySQL
name: etherpad-mysql
fieldref:
fieldpath: metadata.name
configurations:
- transformer-config-kubedb.yaml
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite/README.md
# kubedb-mysql-etherpad-lite
This is *just* the kubedb MySQL resource for etherpad-lite. Compose it with something like ../etherpad-lite-k8s to get a full setup.
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- etherpad-mysql.yaml
vars:
- name: MYSQL_SERVICE
objref:
apiVersion: kubedb.com/v1alpha1
kind: MySQL
name: etherpad-mysql
fieldref:
fieldpath: metadata.name
configurations:
- transformer-config-kubedb.yaml
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite/transformer-config-kubedb.yaml
namePrefix:
- apiVersion: kubedb.com/v1alpha1
kind: MySQL
path: spec/init/scriptSource/configMap/name
nameReference:
- version: v1
kind: ConfigMap
fieldSpecs:
- version: kubedb.com/v1alpha1
kind: MySQL
path: spec/init/scriptSource
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite-with-init-script/etherpad-mysql-init-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: etherpad-mysql-init
data:
init.sql: |
create database `etherpad_lite_db`;
use `etherpad_lite_db`;
CREATE TABLE `store` (
`key` varchar(100) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
`value` longtext COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite-with-init-script/etherpad-mysql-with-init-script.yaml
apiVersion: kubedb.com/v1alpha1
kind: MySQL
metadata:
name: etherpad-mysql
spec:
init:
scriptSource:
configMap:
name: etherpad-mysql-init
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat kubedb-mysql-etherpad-lite-with-init-script/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../kubedb-mysql-etherpad-lite
resources:
- etherpad-mysql-init-configmap.yaml
patchesStrategicMerge:
- etherpad-mysql-with-init-script.yaml
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$ cat test-etherpad-lite-mysql-with-namePrefix/kustomization.yaml
bases:
- ../etherpad-lite-k8s-kubedb-mysql
namePrefix: test-namePrefix-
usuari@CASCA:~/Nextcloud/EC/Documents/fct/k9/serveis/etherpad/etherpad-lite/lib$

View File

@ -0,0 +1,40 @@
# etherpad-lite
Configuration for running [Etherpad Lite](https://github.com/ether/etherpad-lite) on kubernetes.
`./lib/` contains several directories that configure Etherpad Lite in different ways, from the very simplest (but using ephemeral storage) to one that stores ether pads in MySQL provisioned by kubedb.
## Usage
Use `kubectl kustomize <dir>` to render kubernetes manifests, e.g.
```
kubectl kustomize lib/etherpad-lite-k8s
```
or
```
kubectl kustomize lib/etherpad-lite-k8s-kubedb-mysql/
```
or (via URL, no git clone required!)
```
kubectl kustomize github.com/gobengo/etherpad-lite.git/lib/etherpad-lite-k8s
```
## Install on Kubernetes
Assuming you have created a namespace named `my-etherpad-namespace` with something like `kubectl create ns my-etherpad-namespace`
```
kubectl kustomize github.com/gobengo/etherpad-lite.git/lib/etherpad-lite-k8s | kubectl apply -n my-etherpad-namespace -f -
```
If you don't have unix pipes:
```
kubectl -n my-etherpad-namespace apply -k github.com/gobengo/etherpad-lite.git/lib/etherpad-lite-k8s
```

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: etherpad
data:
settings.json: |
{
"skinName":"colibris",
"title":"Etherpad on Kubernetes w/ MySQL",
"dbType": "${ETHERPAD_DB_TYPE:mysql}",
"dbSettings": {
"database": "${ETHERPAD_DB_DATABASE}",
"host": "${ETHERPAD_DB_HOST}",
"password": "${ETHERPAD_DB_PASSWORD}",
"user": "${ETHERPAD_DB_USER}"
}
}

View File

@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: etherpad
spec:
template:
spec:
containers:
- name: etherpad
env:
- name: ETHERPAD_DB_TYPE
value: mysql
- name: ETHERPAD_DB_HOST
value: $(MYSQL_SERVICE)
- name: ETHERPAD_DB_DATABASE
value: etherpad_lite_db
- name: ETHERPAD_DB_USER
valueFrom:
secretKeyRef:
name: etherpad-mysql-auth
key: username
- name: ETHERPAD_DB_PASSWORD
valueFrom:
secretKeyRef:
name: etherpad-mysql-auth
key: password
volumeMounts:
- name: "config"
mountPath: "/opt/etherpad-lite/settings.json"
subPath: "settings.json"

View File

@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../kubedb-mysql-etherpad-lite-with-init-script
- ../etherpad-lite-k8s
patches:
- configmap.yaml
- deployment.yaml
images:
- name: etherpad/etherpad
newTag: latest
configurations:
- name-prefix-transformer-config.yaml

View File

@ -0,0 +1,4 @@
namePrefix:
- apiVersion: apps/v1
kind: Deployment
path: spec/template/spec/containers/env/valueFrom/secretKeyRef/name

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: etherpad
data:
settings.json: |
{
"skinName":"colibris",
"title":"Etherpad on Kubernetes"
}

View File

@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: etherpad
spec:
replicas: 1
selector:
matchLabels:
app: etherpad
template:
metadata:
labels:
app: etherpad
spec:
containers:
- name: etherpad
image: etherpad/etherpad:1.7.5
ports:
- containerPort: 9001
name: web
volumeMounts:
- name: "config"
mountPath: "/opt/etherpad/settings.json"
subPath: "settings.json"
volumes:
- name: config
configMap:
name: etherpad

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
- deployment.yaml
- service.yaml

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: etherpad
spec:
selector:
app: etherpad
ports:
- name: web
port: 80
targetPort: web

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: etherpad-mysql-init
data:
init.sql: |
create database `etherpad_lite_db`;
use `etherpad_lite_db`;
CREATE TABLE `store` (
`key` varchar(100) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
`value` longtext COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

Some files were not shown because too many files have changed in this diff Show More