├── bin └── .gitignore ├── hack └── boilerplate.go.txt ├── config ├── crd │ └── third │ │ └── .gitignore ├── default │ └── kustomization.yaml └── rbac │ ├── service_account.yaml │ ├── kustomization.yaml │ ├── leader_election_role_binding.yaml │ ├── role_binding.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── .dockerignore ├── main.go ├── Dockerfile ├── PROJECT ├── .github ├── ISSUE_TEMPLATE │ ├── issue.md │ └── bug_report.md └── workflows │ ├── main.yaml │ └── release.yaml ├── .gitignore ├── Makefile.versions ├── controllers ├── groupversion_info.go ├── constants.go ├── setup.go ├── suite_test.go ├── httpproxy_controller.go └── httpproxy_controller_test.go ├── docs ├── maintenance.md ├── design.md └── usage.md ├── README.md ├── RELEASE.md ├── cmd ├── run.go └── root.go ├── go.mod ├── Makefile ├── CHANGELOG.md ├── LICENSE └── go.sum /bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/crd/third/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !bin/contour-plus 3 | !LICENSE 4 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: ingress 2 | bases: 3 | - ../rbac 4 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: contour 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cybozu-go/contour-plus/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - service_account.yaml 3 | - role.yaml 4 | - role_binding.yaml 5 | - leader_election_role.yaml 6 | - leader_election_role_binding.yaml 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | LABEL org.opencontainers.image.source="https://github.com/cybozu-go/contour-plus" 3 | 4 | COPY bin/contour-plus /contour-plus 5 | COPY LICENSE /LICENSE 6 | 7 | EXPOSE 8180 8 | USER 10000:10000 9 | 10 | ENTRYPOINT ["/contour-plus"] 11 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: cybozu.com 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: contour-plus 5 | repo: github.com/cybozu-go/contour-plus 6 | resources: 7 | - controller: true 8 | domain: cybozu.com 9 | group: projectcontour.io 10 | kind: HTTPProxy 11 | version: v1 12 | version: "3" 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: contour-plus 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: contour-plus 9 | subjects: 10 | - kind: ServiceAccount 11 | name: contour 12 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: contour-plus 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: contour-plus 9 | subjects: 10 | - kind: ServiceAccount 11 | name: contour 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Describe this issue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What 11 | 12 | Describe what this issue should address. 13 | 14 | ## How 15 | 16 | Describe how to address the issue. 17 | 18 | ## Checklist 19 | 20 | - [ ] Finish implentation of the issue 21 | - [ ] Test all functions 22 | - [ ] Have enough logs to trace activities 23 | - [ ] Notify developers of necessary actions 24 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: contour-plus 5 | rules: 6 | - apiGroups: 7 | - "" 8 | - coordination.k8s.io 9 | resources: 10 | - configmaps 11 | - leases 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - create 17 | - update 18 | - patch 19 | - delete 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - events 24 | verbs: 25 | - create 26 | - patch 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Kubernetes Generated files - skip generated files, except for vendored files 16 | 17 | !vendor/**/zz_generated.* 18 | 19 | # editor and IDE paraphernalia 20 | .idea 21 | *.swp 22 | *.swo 23 | *~ 24 | .#* 25 | \#*# 26 | /.vscode 27 | -------------------------------------------------------------------------------- /Makefile.versions: -------------------------------------------------------------------------------- 1 | ACTIONS_CHECKOUT_VERSION := 6 2 | ACTIONS_CREATE_RELEASE_VERSION := 1 3 | ACTIONS_SETUP_GO_VERSION := 6 4 | CERT_MANAGER_VERSION := 1.19.1 5 | CONTOUR_VERSION := 1.33.0 6 | ENVTEST_K8S_VERSION := 1.34.1 7 | EXTERNAL_DNS_VERSION := 0.20.0 8 | GH_VERSION := 2.68.1 9 | YQ_VERSION := 4.45.1 10 | 11 | # Follow the kustomize version installed in the Argo CD container 12 | # https://github.com/cybozu/neco-containers/blob/main/argocd/Dockerfile#L10 13 | KUSTOMIZE_VERSION := 5.6.0 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - 'main' 7 | jobs: 8 | build: 9 | name: Build image 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions/setup-go@v6 14 | with: 15 | go-version-file: 'go.mod' 16 | - run: make setup 17 | - run: make check-generate 18 | - run: make lint 19 | - run: make test 20 | - run: make docker-build 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Environments** 14 | - Version: 15 | - OS: 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /controllers/groupversion_info.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "k8s.io/apimachinery/pkg/runtime/schema" 4 | 5 | var ( 6 | // externalDNSGroupVersion is external-dns group version which is used to uniquely identifies the API 7 | externalDNSGroupVersion = schema.GroupVersion{Group: "externaldns.k8s.io", Version: "v1alpha1"} 8 | // certManagerGroupVersion is cert-manager group version which is used to uniquely identifies the API 9 | certManagerGroupVersion = schema.GroupVersion{Group: "cert-manager.io", Version: "v1"} 10 | // contourGroupVersion is the contour group version which is used to uniquely identify the API 11 | contourGroupVersion = schema.GroupVersion{Group: "projectcontour.io", Version: "v1"} 12 | ) 13 | -------------------------------------------------------------------------------- /controllers/constants.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // Constants for Kinds 4 | const ( 5 | ClusterIssuerKind = "ClusterIssuer" 6 | IssuerKind = "Issuer" 7 | CertificateKind = "Certificate" 8 | CertificateListKind = "CertificateList" 9 | DNSEndpointKind = "DNSEndpoint" 10 | DNSEndpointListKind = "DNSEndpointList" 11 | TLSCertificateDelegationKind = "TLSCertificateDelegation" 12 | TLSCertificateDelegationListKind = "TLSCertificateDelegationList" 13 | ) 14 | 15 | // Constants for certificate usages 16 | const ( 17 | usageDigitalSignature = "digital signature" 18 | usageKeyEncipherment = "key encipherment" 19 | usageServerAuth = "server auth" 20 | ) 21 | -------------------------------------------------------------------------------- /docs/maintenance.md: -------------------------------------------------------------------------------- 1 | # Maintenance procedure 2 | 3 | 1. Update Contour version in `go.mod`. 4 | It also updates reference to Kubernetes in `go.mod`. 5 | The Kubernetes version is the one used by Contour, but the latest patch version. 6 | ```console 7 | $ make update-contour 8 | ``` 9 | 2. Update `go.mod` for the other dependencies. 10 | 3. Update Go & Ubuntu versions if needed. 11 | 4. Update `CONTROLLER_TOOLS_VERSION` in `Makefile`. 12 | 5. Check for new software versions using `make version`. You may be prompted to login to github.com. 13 | ```console 14 | $ make version 15 | ``` 16 | 6. Check `Makefile.versions` and revert some changes that you don't want now. 17 | 7. Update software versions using `make maintenance`. 18 | ```console 19 | $ make maintenance 20 | ``` 21 | 8. Follow [RELEASE.md](/RELEASE.md) to update software version. 22 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: contour-plus 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - services 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - services/status 19 | verbs: 20 | - get 21 | - apiGroups: 22 | - cert-manager.io 23 | resources: 24 | - certificates 25 | verbs: 26 | - create 27 | - delete 28 | - get 29 | - list 30 | - patch 31 | - update 32 | - watch 33 | - apiGroups: 34 | - externaldns.k8s.io 35 | resources: 36 | - dnsendpoints 37 | verbs: 38 | - create 39 | - delete 40 | - get 41 | - list 42 | - patch 43 | - update 44 | - watch 45 | - apiGroups: 46 | - projectcontour.io 47 | resources: 48 | - httpproxies 49 | verbs: 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - projectcontour.io 57 | resources: 58 | - httpproxies/status 59 | verbs: 60 | - get 61 | - apiGroups: 62 | - projectcontour.io.resources=tlscertificatedelegations 63 | verbs: 64 | - create 65 | - delete 66 | - get 67 | - list 68 | - patch 69 | - update 70 | - watch 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | image: 8 | name: Push container image 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/checkout@v6 12 | - uses: actions/setup-go@v6 13 | with: 14 | go-version-file: 'go.mod' 15 | - run: make setup 16 | - run: make check-generate 17 | - run: make lint 18 | - run: make test 19 | - run: make docker-build 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v3 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | - name: Push versioned image to ghcr.io 27 | run: | 28 | TAG=${GITHUB_REF#refs/tags/v} 29 | docker tag ghcr.io/cybozu-go/contour-plus:latest ghcr.io/cybozu-go/contour-plus:$TAG 30 | docker push ghcr.io/cybozu-go/contour-plus:$TAG 31 | - name: Push latest image to ghcr.io 32 | if: ${{ !contains(github.ref, '-') }} 33 | run: docker push ghcr.io/cybozu-go/contour-plus:latest 34 | release: 35 | name: Release on GitHub 36 | needs: image 37 | runs-on: ubuntu-22.04 38 | steps: 39 | - uses: actions/checkout@v6 40 | - name: Create release 41 | id: create_release 42 | uses: actions/create-release@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | tag_name: ${{ github.ref }} 47 | release_name: Release ${{ github.ref }} 48 | body: | 49 | See [CHANGELOG.md](./CHANGELOG.md) for details. 50 | draft: false 51 | prerelease: ${{ contains(github.ref, '-') }} 52 | -------------------------------------------------------------------------------- /controllers/setup.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | projectcontourv1 "github.com/projectcontour/contour/apis/projectcontour/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 7 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/manager" 11 | // +kubebuilder:scaffold:imports 12 | ) 13 | 14 | // ReconcilerOptions is a set of options for reconcilers 15 | type ReconcilerOptions struct { 16 | ServiceKey client.ObjectKey 17 | Prefix string 18 | DefaultIssuerName string 19 | DefaultIssuerKind string 20 | DefaultDelegatedDomain string 21 | AllowedDelegatedDomains []string 22 | AllowCustomDelegations bool 23 | CSRRevisionLimit uint 24 | CreateDNSEndpoint bool 25 | CreateCertificate bool 26 | IngressClassName string 27 | PropagatedAnnotations []string 28 | PropagatedLabels []string 29 | AllowedDNSNamespaces []string 30 | AllowedIssuerNamespaces []string 31 | } 32 | 33 | // SetupScheme initializes a schema 34 | func SetupScheme(scm *runtime.Scheme) { 35 | utilruntime.Must(clientgoscheme.AddToScheme(scm)) 36 | utilruntime.Must(projectcontourv1.AddToScheme(scm)) 37 | 38 | // +kubebuilder:scaffold:scheme 39 | } 40 | 41 | // SetupReconciler initializes reconcilers 42 | func SetupReconciler(mgr manager.Manager, scheme *runtime.Scheme, opts ReconcilerOptions) error { 43 | httpProxyReconciler := &HTTPProxyReconciler{ 44 | Client: mgr.GetClient(), 45 | Log: ctrl.Log.WithName("controllers").WithName("HTTPProxy"), 46 | Scheme: scheme, 47 | ReconcilerOptions: opts, 48 | } 49 | err := httpProxyReconciler.SetupWithManager(mgr) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // +kubebuilder:scaffold:builder 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/release/cybozu-go/contour-plus.svg?maxAge=60)][releases] 2 | [![CI](https://github.com/cybozu-go/contour-plus/workflows/main/badge.svg)](https://github.com/cybozu-go/contour-plus/actions) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/cybozu-go/contour-plus.svg)](https://pkg.go.dev/github.com/cybozu-go/contour-plus) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/cybozu-go/contour-plus)](https://goreportcard.com/report/github.com/cybozu-go/contour-plus) 5 | 6 | Contour Plus 7 | ============ 8 | 9 | Contour Plus enhances [Contour][] for [ExternalDNS][] and [cert-manager][]. 10 | 11 | **Project Status**: Testing for GA 12 | 13 | Supported environments 14 | ---------------------- 15 | 16 | - Kubernetes 17 | - 1.32 18 | - Contour 19 | - 1.33 20 | - ExternalDNS 21 | - 0.20 22 | - cert-manager 23 | - 1.19 24 | 25 | Other versions may or may not work. 26 | 27 | Features 28 | -------- 29 | 30 | - Create/update/delete [DNSEndpoint][] for ExternalDNS according to FQDN in [HTTPProxy][]. 31 | - Create/update/delete [Certificate][] for cert-manager when [HTTPProxy][] is annotated with `kubernetes.io/tls-acme: true`. 32 | 33 | Other features are described in [docs/usage.md](docs/usage.md). 34 | 35 | Documentation 36 | ------------- 37 | 38 | [docs](docs/) directory contains documents about designs and specifications. 39 | 40 | [releases]: https://github.com/cybozu-go/contour-plus/releases 41 | [godoc]: https://pkg.go.dev/github.com/cybozu-go/contour-plus 42 | [Contour]: https://github.com/projectcontour/contour 43 | [ExternalDNS]: https://github.com/kubernetes-sigs/external-dns 44 | [cert-manager]: https://github.com/cert-manager/cert-manager 45 | [HTTPProxy]: https://projectcontour.io/docs/1.32/config/api/#projectcontour.io/v1.HTTPProxy 46 | [DNSEndpoint]: https://github.com/kubernetes-sigs/external-dns/blob/master/docs/sources/crd.md 47 | [Certificate]: https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.Certificate 48 | 49 | Docker images 50 | ------------- 51 | 52 | Docker images are available on [ghcr.io](https://github.com/cybozu-go/contour-plus/pkgs/container/contour-plus) 53 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | Release procedure 2 | ================= 3 | 4 | This document describes how to release a new version of contour-plus. 5 | 6 | Versioning 7 | ---------- 8 | 9 | Follow [semantic versioning 2.0.0][semver] to choose the new version number. 10 | 11 | Prepare change log entries 12 | -------------------------- 13 | 14 | Add notable changes since the last release to [CHANGELOG.md](CHANGELOG.md). 15 | It should look like: 16 | 17 | ```markdown 18 | (snip) 19 | ## [Unreleased] 20 | 21 | ### Added 22 | - Implement ... (#35) 23 | 24 | ### Changed 25 | - Fix a bug in ... (#33) 26 | 27 | ### Removed 28 | - Deprecated `-option` is removed ... (#39) 29 | 30 | (snip) 31 | ``` 32 | 33 | Bump version 34 | ------------ 35 | 36 | 1. Determine a new version number. Then set `VERSION` variable. 37 | 38 | ```console 39 | # Set VERSION and confirm it. It should not have "v" prefix. 40 | $ VERSION=x.y.z 41 | $ echo $VERSION 42 | ``` 43 | 44 | 2. Make a branch to release 45 | 46 | ```console 47 | $ git checkout main 48 | $ git pull 49 | $ git checkout -b "bump-$VERSION" 50 | ``` 51 | 52 | 3. Edit `CHANGELOG.md` for the new version ([example][]). 53 | 4. Edit `README.md` for the new version ([readme-example][]) if needed. 54 | 5. Commit the change and push it. 55 | 56 | ```console 57 | $ git commit -a -m "Bump version to $VERSION" 58 | $ git push -u origin HEAD 59 | $ gh pr create -f 60 | ``` 61 | 62 | 6. Merge this branch. 63 | 7. Add a git tag to the main HEAD, then push it. 64 | 65 | ```console 66 | # Set VERSION again. 67 | $ VERSION=x.y.z 68 | $ echo $VERSION 69 | 70 | $ git checkout main 71 | $ git pull 72 | $ git tag -a -m "Release v$VERSION" "v$VERSION" 73 | 74 | # Make sure the release tag exists. 75 | $ git tag -ln | grep $VERSION 76 | 77 | $ git push origin "v$VERSION" 78 | ``` 79 | 80 | GitHub actions will build and push artifacts such as container images and 81 | create a new GitHub release. 82 | 83 | [semver]: https://semver.org/spec/v2.0.0.html 84 | [example]: https://github.com/cybozu-go/etcdpasswd/commit/77d95384ac6c97e7f48281eaf23cb94f68867f79 85 | [readme-example]: https://github.com/cybozu-go/contour-plus/commit/858163c5bee47fbf9f5fe2f2a28c7b997e23d7da 86 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | Design notes 2 | ============ 3 | 4 | Background 5 | ---------- 6 | 7 | Contour controlls CRDs called [`HTTProxy`][HTTPProxy]. However, [ExternalDNS][] 8 | and [cert-manager][] does not recognize it unlike the standard `Ingress`. 9 | 10 | Fortunately, ExternalDNS can watch arbitrary CRD resources and manages external 11 | DNS service such as AWS Route53 according to the CRD contents. An example of 12 | such a CRD is [`DNSEndpoint`][DNSEndpoint]. 13 | 14 | Similarly, cert-manager watches [`Certificate`][Certificate] CRD and issues 15 | TLS certificates. 16 | 17 | Goals 18 | ----- 19 | 20 | - Automatic DNS record management for `HTTPProxy` 21 | - Automatic TLS certificate issuance for `HTTPProxy` 22 | 23 | How 24 | --- 25 | 26 | Create a custom controller / operator called `contour-plus` that watches `HTTPProxy` 27 | and IP address of the load balancer (`Service`) for Contour. 28 | 29 | When a new `HTTPProxy` wants a FQDN to be routed, `contour-plus` creates 30 | `DNSEndpoint` for ExternalDNS. If a new `HTTPProxy` wants a TLS certificate, 31 | `contour-plus` creates `Certificate` for cert-manager. 32 | 33 | When an existing `HTTPProxy` is updated or removed, `contour-plus` updates or 34 | deletes corresponding `DNSEndpoint` and/or `Certificate`. 35 | 36 | This way, DNS records can be managed and TLS certificates can be issued automatically. 37 | 38 | ### Access CRDs 39 | 40 | Contour provides Go types and API to manage `HTTPProxy` resource: 41 | 42 | - [`HTTPProxy`][HTTPProxy] 43 | 44 | 45 | cert-manager provides Go types and API to manage `Certificate` resource: 46 | 47 | - [`Certificate`](https://pkg.go.dev/github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1#Certificate) 48 | 49 | ExternalDNS provides Go types for `DNSEndpoint`, but does not provide strictly-typed 50 | API client. Therefore, `contour-plus` uses [kubebuilder][] to generate strictly-typed 51 | API client for itself. 52 | 53 | - [`DNSEndpoint`][DNSEndpoint] 54 | 55 | [HTTPProxy]: https://pkg.go.dev/github.com/projectcontour/contour/apis/projectcontour/v1#HTTPProxy 56 | [ExternalDNS]: https://github.com/kubernetes-sigs/external-dns 57 | [cert-manager]: https://github.com/jetstack/cert-manager 58 | [Certificate]: https://cert-manager.io/docs/usage/certificate/ 59 | [kubebuilder]: https://github.com/kubernetes-sigs/kubebuilder 60 | [DNSEndpoint]: https://pkg.go.dev/github.com/kubernetes-sigs/external-dns/endpoint#DNSEndpoint 61 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/spf13/viper" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | ctrl "sigs.k8s.io/controller-runtime" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 13 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 14 | 15 | "github.com/cybozu-go/contour-plus/controllers" 16 | ) 17 | 18 | var ( 19 | scheme = runtime.NewScheme() 20 | setupLog = ctrl.Log.WithName("setup") 21 | ) 22 | 23 | func init() { 24 | controllers.SetupScheme(scheme) 25 | } 26 | 27 | func run() error { 28 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zapOpts))) 29 | 30 | opts := controllers.ReconcilerOptions{ 31 | Prefix: viper.GetString("name-prefix"), 32 | DefaultIssuerName: viper.GetString("default-issuer-name"), 33 | } 34 | 35 | crds := viper.GetStringSlice("crds") 36 | if len(crds) == 0 { 37 | return errors.New("at least one service need to be enabled") 38 | } 39 | for _, crd := range crds { 40 | switch crd { 41 | case controllers.DNSEndpointKind: 42 | opts.CreateDNSEndpoint = true 43 | case controllers.CertificateKind: 44 | opts.CreateCertificate = true 45 | default: 46 | return errors.New("unsupported CRD: " + crd) 47 | } 48 | } 49 | 50 | serviceName := viper.GetString("service-name") 51 | nsname := strings.Split(serviceName, "/") 52 | if len(nsname) != 2 || nsname[0] == "" || nsname[1] == "" { 53 | return errors.New("service-name should be valid string as namespaced-name") 54 | } 55 | opts.ServiceKey = client.ObjectKey{ 56 | Namespace: nsname[0], 57 | Name: nsname[1], 58 | } 59 | 60 | defaultIssuerKind := viper.GetString("default-issuer-kind") 61 | switch defaultIssuerKind { 62 | case controllers.IssuerKind, controllers.ClusterIssuerKind: 63 | default: 64 | return errors.New("unsupported Issuer kind: " + defaultIssuerKind) 65 | } 66 | opts.DefaultIssuerKind = defaultIssuerKind 67 | 68 | opts.IngressClassName = viper.GetString("ingress-class-name") 69 | 70 | opts.CSRRevisionLimit = viper.GetUint("csr-revision-limit") 71 | 72 | opts.PropagatedAnnotations = viper.GetStringSlice("propagated-annotations") 73 | opts.PropagatedLabels = viper.GetStringSlice("propagated-labels") 74 | 75 | opts.DefaultDelegatedDomain = viper.GetString("default-delegated-domain") 76 | opts.AllowCustomDelegations = viper.GetBool("allow-custom-delegations") 77 | opts.AllowedDelegatedDomains = viper.GetStringSlice("allowed-delegated-domains") 78 | 79 | opts.AllowedDNSNamespaces = viper.GetStringSlice("allowed-dns-namespaces") 80 | opts.AllowedIssuerNamespaces = viper.GetStringSlice("allowed-certificate-namespaces") 81 | 82 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 83 | Scheme: scheme, 84 | Metrics: metricsserver.Options{ 85 | BindAddress: viper.GetString("metrics-addr"), 86 | }, 87 | LeaderElection: viper.GetBool("leader-election"), 88 | LeaderElectionID: "contour-plus-leader", 89 | }) 90 | if err != nil { 91 | setupLog.Error(err, "unable to start manager") 92 | return err 93 | } 94 | 95 | err = controllers.SetupReconciler(mgr, mgr.GetScheme(), opts) 96 | if err != nil { 97 | setupLog.Error(err, "unable to create controllers") 98 | os.Exit(1) 99 | } 100 | 101 | setupLog.Info("starting manager") 102 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 103 | setupLog.Error(err, "problem running manager") 104 | os.Exit(1) 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/pflag" 13 | "github.com/spf13/viper" 14 | "k8s.io/klog/v2" 15 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 16 | 17 | "github.com/cybozu-go/contour-plus/controllers" 18 | // +kubebuilder:scaffold:imports 19 | ) 20 | 21 | var zapOpts zap.Options 22 | 23 | // Execute adds all child commands to the root command and sets flags appropriately. 24 | // This is called by main.main(). It only needs to happen once to the rootCmd. 25 | func Execute() { 26 | if err := rootCmd.Execute(); err != nil { 27 | fmt.Println(err) 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | func init() { 33 | controllers.SetupScheme(scheme) 34 | 35 | fs := rootCmd.Flags() 36 | fs.String("metrics-addr", ":8180", "Bind address for the metrics endpoint") 37 | fs.StringSlice("crds", []string{controllers.DNSEndpointKind, controllers.CertificateKind}, "List of CRD names to be created") 38 | fs.String("name-prefix", "", "Prefix of CRD names to be created") 39 | fs.String("service-name", "", "NamespacedName of the Contour LoadBalancer Service") 40 | fs.String("default-issuer-name", "", "Issuer name used by default") 41 | fs.String("default-issuer-kind", controllers.ClusterIssuerKind, "Issuer kind used by default") 42 | fs.String("default-delegated-domain", "", "Delegated domain used by default") 43 | fs.StringSlice("allowed-delegated-domains", []string{}, "List of allowed delegated domains") 44 | fs.Bool("allow-custom-delegations", false, "Allow custom delegated domains via annotations") 45 | fs.Uint("csr-revision-limit", 0, "Maximum number of CertificateRequest revisions to keep") 46 | fs.String("ingress-class-name", "", "Ingress class name that watched by Contour Plus. If not specified, then all classes are watched") 47 | fs.Bool("leader-election", true, "Enable/disable leader election") 48 | fs.StringSlice("propagated-annotations", []string{}, "List of annotation keys to be propagated from HTTPProxy to generated resources") 49 | fs.StringSlice("propagated-labels", []string{}, "List of label keys to be propagated from HTTPProxy to generated resources") 50 | fs.StringSlice("allowed-dns-namespaces", []string{}, "List of namespaces where DNSEndpoint resources can be created. If empty, no namespaces are allowed") 51 | fs.StringSlice("allowed-issuer-namespaces", []string{}, "List of namespaces where Certificate resources can be created. If empty, no namespaces are allowed") 52 | if err := viper.BindPFlags(fs); err != nil { 53 | panic(err) 54 | } 55 | envKeyReplacer := strings.NewReplacer("-", "_") 56 | viper.SetEnvPrefix("cp") 57 | viper.SetEnvKeyReplacer(envKeyReplacer) 58 | viper.AutomaticEnv() 59 | 60 | // Because k8s.io/klog uses Go flag package, we need to add flags for klog to fs. 61 | goflags := flag.NewFlagSet("klog", flag.ExitOnError) 62 | klog.InitFlags(goflags) 63 | zapOpts.BindFlags(goflags) 64 | 65 | fs.AddGoFlagSet(goflags) 66 | rootCmd.Long = rootCmd.Short + "\n\n" + generateEnvDoc(fs, envKeyReplacer) 67 | } 68 | 69 | func generateEnvDoc(fs *pflag.FlagSet, replacer *strings.Replacer) string { 70 | var buf bytes.Buffer 71 | w := tabwriter.NewWriter(&buf, 0, 0, 4, ' ', 0) 72 | _, _ = w.Write([]byte("In addition to flags, the following environment variables are read:\n\n")) 73 | fs.VisitAll(func(f *pflag.Flag) { 74 | envName := "CP_" + strings.ToUpper(replacer.Replace(f.Name)) 75 | fmt.Fprintf(w, "\t\t%s\t%s\n", envName, f.Usage) 76 | }) 77 | w.Flush() 78 | return buf.String() 79 | } 80 | 81 | var rootCmd = &cobra.Command{ 82 | Use: "contour-plus", 83 | Short: "contour-plus is a custom controller for Contour HTTPProxy", 84 | RunE: func(cmd *cobra.Command, args []string) error { 85 | cmd.SilenceUsage = true 86 | return run() 87 | }, 88 | } 89 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/utils/ptr" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/config" 19 | "sigs.k8s.io/controller-runtime/pkg/envtest" 20 | logf "sigs.k8s.io/controller-runtime/pkg/log" 21 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 22 | "sigs.k8s.io/controller-runtime/pkg/manager" 23 | // +kubebuilder:scaffold:imports 24 | ) 25 | 26 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 27 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 28 | 29 | var ( 30 | cfg *rest.Config 31 | k8sClient client.Client 32 | testEnv *envtest.Environment 33 | 34 | testServiceKey = client.ObjectKey{Namespace: "test-ns", Name: "test-svc"} 35 | ) 36 | 37 | const ( 38 | testNamespacePrefix = "test-ns-" 39 | dummyLoadBalancerIP = "10.0.0.0" 40 | ) 41 | 42 | func TestAPIs(t *testing.T) { 43 | RegisterFailHandler(Fail) 44 | 45 | RunSpecs(t, "Controller Suite") 46 | } 47 | 48 | var _ = BeforeSuite(func() { 49 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 50 | 51 | By("bootstrapping test environment") 52 | testEnv = &envtest.Environment{ 53 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "third")}, 54 | } 55 | 56 | c, err := testEnv.Start() 57 | cfg = c 58 | Expect(err).NotTo(HaveOccurred()) 59 | Expect(cfg).NotTo(BeNil()) 60 | 61 | By("setting up scheme") 62 | scheme := runtime.NewScheme() 63 | SetupScheme(scheme) 64 | 65 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 66 | Expect(err).NotTo(HaveOccurred()) 67 | Expect(k8sClient).NotTo(BeNil()) 68 | 69 | By("creating namespace") 70 | namespace := &corev1.Namespace{ 71 | ObjectMeta: ctrl.ObjectMeta{ 72 | Name: testServiceKey.Namespace, 73 | }, 74 | } 75 | Expect(k8sClient.Create(context.Background(), namespace)).ShouldNot(HaveOccurred()) 76 | 77 | By("creating httpproy loadbalancer service") 78 | svc := &corev1.Service{ 79 | ObjectMeta: ctrl.ObjectMeta{ 80 | Namespace: testServiceKey.Namespace, 81 | Name: testServiceKey.Name, 82 | }, 83 | Spec: corev1.ServiceSpec{ 84 | Ports: []corev1.ServicePort{{Port: 8080}}, 85 | LoadBalancerIP: dummyLoadBalancerIP, 86 | Type: corev1.ServiceTypeLoadBalancer, 87 | }, 88 | } 89 | Expect(k8sClient.Create(context.Background(), svc)).ShouldNot(HaveOccurred()) 90 | svc.Status = corev1.ServiceStatus{ 91 | LoadBalancer: corev1.LoadBalancerStatus{ 92 | Ingress: []corev1.LoadBalancerIngress{{ 93 | IP: dummyLoadBalancerIP, 94 | }}, 95 | }, 96 | } 97 | Expect(k8sClient.Status().Update(context.Background(), svc)).ShouldNot(HaveOccurred()) 98 | }) 99 | 100 | var _ = AfterSuite(func() { 101 | By("tearing down the test environment") 102 | err := testEnv.Stop() 103 | Expect(err).NotTo(HaveOccurred()) 104 | }) 105 | 106 | var _ = Describe("Test contour-plus", func() { 107 | Context("httpproxy", testHTTPProxyReconcile) 108 | }) 109 | 110 | func startTestManager(mgr manager.Manager) (stop func()) { 111 | waitCh := make(chan struct{}) 112 | ctx, cancel := context.WithCancel(context.Background()) 113 | stop = func() { 114 | cancel() 115 | <-waitCh 116 | } 117 | go func() { 118 | err := mgr.Start(ctx) 119 | if err != nil { 120 | panic(err) 121 | } 122 | close(waitCh) 123 | }() 124 | time.Sleep(100 * time.Millisecond) 125 | return 126 | } 127 | 128 | func setupManager() (*runtime.Scheme, manager.Manager) { 129 | scm := runtime.NewScheme() 130 | SetupScheme(scm) 131 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 132 | Scheme: scm, 133 | Controller: config.Controller{ 134 | SkipNameValidation: ptr.To(true), 135 | }, 136 | }) 137 | Expect(err).ShouldNot(HaveOccurred()) 138 | return scm, mgr 139 | } 140 | 141 | func randomString(n int) string { 142 | var letter = []rune("abcdefghijklmnopqrstuvwxyz") 143 | 144 | b := make([]rune, n) 145 | for i := range b { 146 | b[i] = letter[rand.Intn(len(letter))] 147 | } 148 | return string(b) 149 | } 150 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cybozu-go/contour-plus 2 | 3 | go 1.25.4 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.3 7 | github.com/onsi/ginkgo/v2 v2.25.3 8 | github.com/onsi/gomega v1.38.2 9 | github.com/projectcontour/contour v1.33.0 10 | github.com/spf13/cobra v1.10.2 11 | github.com/spf13/pflag v1.0.10 12 | github.com/spf13/viper v1.21.0 13 | k8s.io/api v0.34.2 14 | k8s.io/apimachinery v0.34.2 15 | k8s.io/client-go v0.34.2 16 | k8s.io/klog/v2 v2.130.1 17 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 18 | sigs.k8s.io/controller-runtime v0.22.4 19 | ) 20 | 21 | require ( 22 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/emicklei/go-restful/v3 v3.13.0 // indirect 27 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 28 | github.com/fsnotify/fsnotify v1.9.0 // indirect 29 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 30 | github.com/go-logr/zapr v1.3.0 // indirect 31 | github.com/go-openapi/jsonpointer v0.22.3 // indirect 32 | github.com/go-openapi/jsonreference v0.21.3 // indirect 33 | github.com/go-openapi/swag v0.25.4 // indirect 34 | github.com/go-openapi/swag/cmdutils v0.25.4 // indirect 35 | github.com/go-openapi/swag/conv v0.25.4 // indirect 36 | github.com/go-openapi/swag/fileutils v0.25.4 // indirect 37 | github.com/go-openapi/swag/jsonname v0.25.4 // indirect 38 | github.com/go-openapi/swag/jsonutils v0.25.4 // indirect 39 | github.com/go-openapi/swag/loading v0.25.4 // indirect 40 | github.com/go-openapi/swag/mangling v0.25.4 // indirect 41 | github.com/go-openapi/swag/netutils v0.25.4 // indirect 42 | github.com/go-openapi/swag/stringutils v0.25.4 // indirect 43 | github.com/go-openapi/swag/typeutils v0.25.4 // indirect 44 | github.com/go-openapi/swag/yamlutils v0.25.4 // indirect 45 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 46 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/google/btree v1.1.3 // indirect 49 | github.com/google/gnostic-models v0.7.1 // indirect 50 | github.com/google/go-cmp v0.7.0 // indirect 51 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 52 | github.com/google/uuid v1.6.0 // indirect 53 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 56 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 58 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 60 | github.com/prometheus/client_golang v1.23.2 // indirect 61 | github.com/prometheus/client_model v0.6.2 // indirect 62 | github.com/prometheus/common v0.67.4 // indirect 63 | github.com/prometheus/procfs v0.19.2 // indirect 64 | github.com/sagikazarmark/locafero v0.12.0 // indirect 65 | github.com/spf13/afero v1.15.0 // indirect 66 | github.com/spf13/cast v1.10.0 // indirect 67 | github.com/subosito/gotenv v1.6.0 // indirect 68 | github.com/x448/float16 v0.8.4 // indirect 69 | go.uber.org/automaxprocs v1.6.0 // indirect 70 | go.uber.org/multierr v1.11.0 // indirect 71 | go.uber.org/zap v1.27.1 // indirect 72 | go.yaml.in/yaml/v2 v2.4.3 // indirect 73 | go.yaml.in/yaml/v3 v3.0.4 // indirect 74 | golang.org/x/net v0.47.0 // indirect 75 | golang.org/x/oauth2 v0.33.0 // indirect 76 | golang.org/x/sync v0.18.0 // indirect 77 | golang.org/x/sys v0.38.0 // indirect 78 | golang.org/x/term v0.37.0 // indirect 79 | golang.org/x/text v0.31.0 // indirect 80 | golang.org/x/time v0.14.0 // indirect 81 | golang.org/x/tools v0.38.0 // indirect 82 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 83 | google.golang.org/protobuf v1.36.10 // indirect 84 | gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 85 | gopkg.in/inf.v0 v0.9.1 // indirect 86 | k8s.io/apiextensions-apiserver v0.34.2 // indirect 87 | k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect 88 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 89 | sigs.k8s.io/randfill v1.0.0 // indirect 90 | sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect 91 | sigs.k8s.io/yaml v1.6.0 // indirect 92 | ) 93 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.versions 2 | 3 | CONTROLLER_TOOLS_VERSION = 0.19.0 4 | 5 | PROJECT_DIR := $(CURDIR) 6 | BIN_DIR := $(PROJECT_DIR)/bin 7 | CRD_DIR := $(PROJECT_DIR)/config/crd/third 8 | WORKFLOWS_DIR := $(PROJECT_DIR)/.github/workflows 9 | 10 | KUSTOMIZE := $(BIN_DIR)/kustomize 11 | CONTROLLER_GEN := $(BIN_DIR)/controller-gen 12 | SETUP_ENVTEST := $(BIN_DIR)/setup-envtest 13 | STATICCHECK := $(BIN_DIR)/staticcheck 14 | CUSTOMCHECKER := $(BIN_DIR)/custom-checker 15 | GOIMPORTS := $(BIN_DIR)/goimports 16 | GH := $(BIN_DIR)/gh 17 | YQ := $(BIN_DIR)/yq 18 | 19 | # Image URL to use all building/pushing image targets 20 | IMG ?= ghcr.io/cybozu-go/contour-plus:latest 21 | 22 | # Set the shell used to bash for better error handling. 23 | SHELL = /bin/bash 24 | .SHELLFLAGS = -e -o pipefail -c 25 | 26 | .PHONY: all 27 | all: help 28 | 29 | ##@ Basic 30 | .PHONY: help 31 | help: ## Display this help 32 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 33 | 34 | .PHONY: setup 35 | setup: download-tools download-crds ## Setup 36 | 37 | .PHONY: download-tools 38 | download-tools: $(GH) $(YQ) 39 | GOBIN=$(BIN_DIR) go install sigs.k8s.io/controller-tools/cmd/controller-gen@v$(CONTROLLER_TOOLS_VERSION) 40 | GOBIN=$(BIN_DIR) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 41 | GOBIN=$(BIN_DIR) go install sigs.k8s.io/kustomize/kustomize/v5@v$(KUSTOMIZE_VERSION) 42 | GOBIN=$(BIN_DIR) go install github.com/cybozu-go/golang-custom-analyzer/cmd/custom-checker@latest 43 | GOBIN=$(BIN_DIR) go install honnef.co/go/tools/cmd/staticcheck@latest 44 | GOBIN=$(BIN_DIR) go install golang.org/x/tools/cmd/goimports@latest 45 | 46 | .PHONY: download-crds 47 | download-crds: 48 | curl -fsL -o $(CRD_DIR)/certmanager.yml -sLf https://github.com/jetstack/cert-manager/releases/download/v$(CERT_MANAGER_VERSION)/cert-manager.crds.yaml 49 | curl -fsL -o $(CRD_DIR)/dnsendpoint.yml -sLf https://github.com/kubernetes-sigs/external-dns/raw/v$(EXTERNAL_DNS_VERSION)/config/crd/standard/dnsendpoints.externaldns.k8s.io.yaml 50 | curl -fsL -o $(CRD_DIR)/httpproxy.yml -sLf https://github.com/projectcontour/contour/raw/v$(CONTOUR_VERSION)/examples/contour/01-crds.yaml 51 | 52 | $(GH): 53 | mkdir -p $(BIN_DIR) 54 | wget -qO - https://github.com/cli/cli/releases/download/v$(GH_VERSION)/gh_$(GH_VERSION)_linux_amd64.tar.gz | tar -zx -O gh_$(GH_VERSION)_linux_amd64/bin/gh > $@ 55 | chmod +x $@ 56 | 57 | $(YQ): 58 | mkdir -p $(BIN_DIR) 59 | wget -qO $@ https://github.com/mikefarah/yq/releases/download/v$(YQ_VERSION)/yq_linux_amd64 60 | chmod +x $@ 61 | 62 | .PHONY: clean 63 | clean: ## Clean files 64 | rm -rf $(BIN_DIR)/* $(CRD_DIR)/* 65 | 66 | ##@ Build 67 | 68 | .PHONY: manifests 69 | manifests: ## Generate manifests e.g. CRD, RBAC etc. 70 | $(CONTROLLER_GEN) rbac:roleName=contour-plus webhook paths="./..." 71 | 72 | .PHONY: generate 73 | generate: ## Generate code 74 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 75 | 76 | .PHONY: build 77 | build: ## Build manager binary 78 | CGO_ENABLED=0 go build -o bin/contour-plus -ldflags="-w -s" main.go 79 | 80 | .PHONY: docker-build 81 | docker-build: build ## Build the docker image 82 | docker build . -t ${IMG} 83 | 84 | ##@ Maintenance 85 | .PHONY: login-gh 86 | login-gh: ## Login to GitHub 87 | if ! $(GH) auth status 2>/dev/null; then \ 88 | echo; \ 89 | echo '!! You need login to GitHub to proceed. Please follow the next command with "Authenticate Git with your GitHub credentials? (Y)".'; \ 90 | echo; \ 91 | $(GH) auth login -h github.com -p HTTPS -w; \ 92 | fi 93 | 94 | .PHONY: logout-gh 95 | logout-gh: ## Logout from GitHub 96 | $(GH) auth logout 97 | 98 | .PHONY: update-contour 99 | update-contour: ## Update Contour and Kubernetes in go.mod 100 | $(call get-latest-gh-package-tag,contour) 101 | go get github.com/projectcontour/contour@$(call upstream-tag,$(latest_tag)) 102 | K8S_MINOR_VERSION="0."$$(go list -m -f '{{.Version}}' k8s.io/api | cut -d'.' -f2); \ 103 | K8S_PACKAGE_VERSION="$$(go list -m -versions k8s.io/api | tr ' ' '\n' | grep $${K8S_MINOR_VERSION} | sort -V | tail -n 1)"; \ 104 | go get k8s.io/api@$${K8S_PACKAGE_VERSION}; \ 105 | go get k8s.io/apimachinery@$${K8S_PACKAGE_VERSION}; \ 106 | go get k8s.io/client-go@$${K8S_PACKAGE_VERSION}; \ 107 | go mod tidy 108 | 109 | .PHONY: version 110 | version: login-gh ## Update dependent versions 111 | $(call update-version,actions/checkout,ACTIONS_CHECKOUT_VERSION,1) 112 | $(call update-version,actions/create-release,ACTIONS_CREATE_RELEASE_VERSION,1) 113 | $(call update-version,actions/setup-go,ACTIONS_SETUP_GO_VERSION,1) 114 | $(call update-version-ghcr,cert-manager,CERT_MANAGER_VERSION) 115 | $(call update-version-ghcr,contour,CONTOUR_VERSION) 116 | $(call update-version-ghcr,external-dns,EXTERNAL_DNS_VERSION) 117 | 118 | $(call get-latest-gh-package-tag,argocd) 119 | NEW_VERSION=$$(docker run ghcr.io/cybozu/argocd:$(latest_tag) kustomize version | cut -c2-); \ 120 | sed -i -e "s/KUSTOMIZE_VERSION := .*/KUSTOMIZE_VERSION := $${NEW_VERSION}/g" Makefile.versions 121 | 122 | K8S_MINOR_VERSION="1."$$(go list -m -f '{{.Version}}' k8s.io/api | cut -d'.' -f2); \ 123 | NEW_VERSION=$$($(SETUP_ENVTEST) list | tr -s ' ' | cut -d' ' -f2 | fgrep $${K8S_MINOR_VERSION} | sort -V | tail -n 1 | cut -c2-); \ 124 | sed -i -e "s/ENVTEST_K8S_VERSION := .*/ENVTEST_K8S_VERSION := $${NEW_VERSION}/g" Makefile.versions 125 | 126 | .PHONY: update-actions 127 | update-actions: 128 | $(call update-trusted-action,actions/checkout,$(ACTIONS_CHECKOUT_VERSION)) 129 | $(call update-trusted-action,actions/create-release,$(ACTIONS_CREATE_RELEASE_VERSION)) 130 | $(call update-trusted-action,actions/setup-go,$(ACTIONS_SETUP_GO_VERSION)) 131 | 132 | .PHONY: maintenance 133 | maintenance: ## Update dependent manifests 134 | $(MAKE) update-actions 135 | $(MAKE) download-crds 136 | 137 | .PHONY: list-actions 138 | list-actions: ## List used GitHub Actions 139 | @{ for i in $(shell ls $(WORKFLOWS_DIR)); do \ 140 | $(YQ) '.. | select(has("uses")).uses' $(WORKFLOWS_DIR)/$$i; \ 141 | done } | sort | uniq 142 | 143 | ##@ Test 144 | 145 | .PHONY: check-generate 146 | check-generate: ## Check for commit omissions of auto-generated files 147 | $(MAKE) manifests 148 | $(MAKE) generate 149 | $(GOIMPORTS) -w -local github.com/cybozu-go/contour-plus . 150 | go mod tidy 151 | git diff --exit-code --name-only 152 | 153 | .PHONY: lint 154 | lint: ## Run lint tools 155 | test -z "$$(gofmt -s -l . | tee /dev/stderr)" 156 | $(STATICCHECK) ./... 157 | test -z "$$($(CUSTOMCHECKER) -restrictpkg.packages=html/template,log $$(go list -tags='$(GOTAGS)' ./... ) 2>&1 | tee /dev/stderr)" 158 | go vet ./... 159 | 160 | .PHONY: test 161 | test: ## Run unit tests 162 | source <($(SETUP_ENVTEST) use -p env $(ENVTEST_K8S_VERSION)) && \ 163 | go test -race -v -count 1 ./... 164 | 165 | # usage get-latest-gh OWNER/REPO 166 | define get-latest-gh 167 | $(eval latest_gh := $(shell $(GH) release list --repo $1 | grep Latest | cut -f3)) 168 | endef 169 | 170 | # usage: get-latest-gh-package-tag NAME 171 | define get-latest-gh-package-tag 172 | $(eval latest_tag := $(shell curl -sSf -H "Authorization: Bearer $(shell curl -sSf "https://ghcr.io/token?scope=repository%3Acybozu%2F$1%3Apull&service=ghcr.io" | jq -r .token)" https://ghcr.io/v2/cybozu/$1/tags/list | jq -r '.tags[]' | sort -Vr | head -n 1)) 173 | endef 174 | 175 | # usage: upstream-tag 1.2.3.4 176 | # do not indent because it appears on output 177 | define upstream-tag 178 | $(shell echo $1 | sed -E 's/^(.*)\.[[:digit:]]+$$/v\1/') 179 | endef 180 | 181 | # usage update-version OWNER/REPO VAR MAJOR 182 | define update-version 183 | $(call get-latest-gh,$1) 184 | NEW_VERSION=$$(echo $(latest_gh) | if [ -z "$3" ]; then cut -b 2-; else cut -b 2; fi); \ 185 | sed -i -e "s/$2 := .*/$2 := $${NEW_VERSION}/g" Makefile.versions 186 | endef 187 | 188 | # usage update-version-ghcr NAME VAR 189 | define update-version-ghcr 190 | $(call get-latest-gh-package-tag,$1) 191 | NEW_VERSION=$$(echo $(call upstream-tag,$(latest_tag)) | cut -b 2-); \ 192 | sed -i -e "s/$2 := .*/$2 := $${NEW_VERSION}/g" Makefile.versions 193 | endef 194 | 195 | # usage update-trusted-action OWNER/REPO VERSION 196 | define update-trusted-action 197 | for i in $(shell ls $(WORKFLOWS_DIR)); do \ 198 | $(YQ) -i '(.. | select(has("uses")) | select(.uses | contains("$1"))).uses = "$1@v$2"' $(WORKFLOWS_DIR)/$$i; \ 199 | done 200 | endef 201 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | contour-plus is an add-on controller for [Contour][]'s [HTTPProxy][]. 5 | 6 | It helps integration of Contour with [external-dns][] and [cert-manager][]. 7 | 8 | Command-line flags and environment variables 9 | -------------------------------------------- 10 | 11 | contour-plus takes following command-line flags or environment variables. 12 | If both is specified, command-line flags take precedence. 13 | 14 | | Flag | Envvar | Default | Description | 15 | | --------------------- | ------------------------ | ------------------------- | -------------------------------------------------- | 16 | | `metrics-addr` | `CP_METRICS_ADDR` | :8180 | Bind address for the metrics endpoint | 17 | | `crds` | `CP_CRDS` | `DNSEndpoint,Certificate` | Comma-separated list of CRDs to be created. | 18 | | `name-prefix` | `CP_NAME_PREFIX` | "" | Prefix of CRD names to be created | 19 | | `service-name` | `CP_SERVICE_NAME` | "" | NamespacedName of the Contour LoadBalancer Service | 20 | | `default-issuer-name` | `CP_DEFAULT_ISSUER_NAME` | "" | Issuer name used by default | 21 | | `default-issuer-kind` | `CP_DEFAULT_ISSUER_KIND` | `ClusterIssuer` | Issuer kind used by default | 22 | | `default-delegated-domain` | `CP_DEFAULT_DELEGATED_DOMAIN` | "" | Domain to which DNS-01 validation is delegated to | 23 | | `allowed-delegated-domains` | `CP_ALLOWED_DELEGATED_DOMAINS` | [] | Comma-separated list of allowed delegated domains | 24 | | `allow-custom-delegations` | `CP_ALLOW_CUSTOM_DELEGATIONS` | `false` | Allow users to specify a custom delegated domain | 25 | | `csr-revision-limit` | `CP_CSR_REVISION_LIMIT` | 0 | Maximum number of CertificateRequests to be kept for a Certificate. By default, all CertificateRequests are kept | 26 | | `leader-election` | `CP_LEADER_ELECTION` | `true` | Enable / disable leader election | 27 | | `ingress-class-name` | `CP_INGRESS_CLASS_NAME` | "" | Ingress class name that watched by Contour Plus. If not specified, then all classes are watched | 28 | | `propagated-annotations` | `CP_PROPAGATED_ANNOTATIONS` | "" | Comma-separated list of annotation keys that should be propagated to the resources contour-plus generates | 29 | | `propagated-labels ` | `CP_PROPAGATED_LABELS` | "" | Comma-separated list of label keys that should be propagated to the resources contour-plus generates | 30 | | `allowed-dns-namespaces` | `CP_ALLOWED_DNS_NAMESPACES` | "" | List of namespaces where DNSEndpoint resources can be created. If empty, no namespaces are allowed | 31 | | `allowed-issuer-namespaces` | `CP_ALLOWED_ISSUER_NAMESPACES` | "" | List of namespaces where Certificate resources can be created. If empty, no namespaces are allowed | 32 | 33 | By default, contour-plus creates [DNSEndpoint][] when `spec.virtualhost.fqdn` of an HTTPProxy is not empty, 34 | and creates [Certificate][] when `spec.virtualhost.tls.secretName` is not empty and not namespaced. 35 | 36 | When a delegated domain is specified, either via `default-delegated-domain` or the `contour-plus.cybozu.com/delegated-domain` annotation, contour-plus creates an additional [DNSEndpoint][] delegating DNS-01 validation to the given delegation domain. The delegation record will not be created if the DNSEndpoint for `spec.virtualhost.fqdn` cannot be created. If `allow-custom-delegations` is enabled, users will be able to specify a custom domain for delegation via the `contour-plus.cybozu.com/delegated-domain` annotation. To prevent users from being able to specify any arbitrary delegation domains, `allowed-delegated-domains` can be used to specify a list of permitted domains. 37 | 38 | To disable CRD creation, specify `crds` command-line flag or `CP_CRDS` environment variable. 39 | 40 | `service-name` is a required flag/envvar that must be the namespaced name of Service for Contour. 41 | In a normal setup, Contour has a `type=LoadBalancer` Service to expose its Envoy pods to Internet. 42 | By specifying `service-name`, contour-plus can identify the global IP address for FQDNs in HTTPProxy. 43 | 44 | If `ingress-class-name` is specified, contour-plus watches only HTTPProxy annotated by `kubernetes.io/ingress.class=`, `projectcontour.io/ingress.class=` or with the `HTTPProxy.Spec.IngressClassName` field that matches the given `ingress-class-name`. 45 | **If `kubernetes.io/ingress.class=` , `projectcontour.io/ingress.class=` and `HTTPProxy.Spec.IngressClassName` are all specified and those values are different from the given `ingress-class-name`, then contour-plus doesn't watch the resource.** 46 | 47 | It is possible to specify different namespaces to install the `DNSEndpoint` and/or `Certificate` resources via annotations. That behavior is constrained via the `allowed-dns-namespaces` and `allowed-issuer-namespaces` flags. 48 | 49 | How it works 50 | ------------ 51 | 52 | contour-plus monitors events for [HTTPProxy][] and creates / updates / deletes 53 | [DNSEndpoint][] and/or [Certificate][]. 54 | 55 | The container of contour-plus should be deployed as a sidecar of Contour/Envoy Pod. 56 | 57 | ### Leader election 58 | 59 | Unless `--leader-election` is set to `false`, contour-plus does leader election using 60 | ConfigMap in the same namespace where its Pod exists. In addition, it creates 61 | Event to log leader election activity. 62 | 63 | Therefore, the service account for Contour need to be bound to a Role like this: 64 | 65 | ```yaml 66 | apiVersion: rbac.authorization.k8s.io/v1 67 | kind: Role 68 | metadata: 69 | name: contour-plus 70 | namespace: ingress 71 | rules: 72 | - apiGroups: 73 | - "" 74 | resources: 75 | - configmaps 76 | verbs: 77 | - get 78 | - list 79 | - watch 80 | - create 81 | - update 82 | - patch 83 | - delete 84 | - apiGroups: 85 | - "" 86 | resources: 87 | - configmaps/status 88 | verbs: 89 | - get 90 | - update 91 | - patch 92 | - apiGroups: 93 | - "" 94 | resources: 95 | - events 96 | verbs: 97 | - create 98 | - apiGroups: 99 | - coordination.k8s.io 100 | resources: 101 | - leases 102 | verbs: 103 | - '*' 104 | ``` 105 | 106 | ### Certificate RBAC 107 | 108 | The following permissions are needed to create/update `Certificates` 109 | 110 | ```yaml 111 | apiVersion: rbac.authorization.k8s.io/v1 112 | kind: Role 113 | metadata: 114 | name: contour-plus 115 | namespace: ingress 116 | rules: 117 | - apiGroups: 118 | - cert-manager.io 119 | resources: 120 | - certificates 121 | verbs: 122 | - get 123 | - list 124 | - watch 125 | - patch 126 | - create 127 | ``` 128 | 129 | ### Supported annotations 130 | 131 | contour-plus interprets following annotations for HTTPProxy. 132 | 133 | - `contour-plus.cybozu.com/exclude: "true"` - With this, contour-plus ignores this HTTPProxy. 134 | - `cert-manager.io/issuer` - The name of an [Issuer][] to acquire the certificate required for this HTTPProxy from. The Issuer must be in the same namespace as the HTTPProxy. 135 | - `cert-manager.io/cluster-issuer` - The name of a [ClusterIssuer][Issuer] to acquire the certificate required for this ingress from. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. 136 | - `cert-manager.io/revision-history-limit` - The maximum number of CertificateRequests to keep for a given Certificate. 137 | - `cert-manager.io/private-key-algorithm` - The algorithm for the private key generation for a Certificate. 138 | - `cert-manager.io/private-key-size` - If `cert-manager.io/private-key-algorithm` is set, this annotation allows the specification of the size of the private key. 139 | - `kubernetes.io/tls-acme: "true"` - With this, contour-plus generates Certificate automatically from HTTPProxy. 140 | - `contour-plus.cybozu.com/delegated-domain: "acme.example.com"` - With this, contour-plus generates a [DNSEndpoint][] to create a CNAME record pointing to the delegation domain for use when performing DNS-01 DCV during the Certificate creation. 141 | - `contour-plus.cybozu.com/dns-namespace` - The namespace in which contour-plus will place a DNSEndpoint. 142 | - `contour-plus.cybozu.com/issuer-namespace` - The namespace in which contour-plus will place a Certificate. 143 | 144 | If both of `cert-manager.io/issuer` and `cert-manager.io/cluster-issuer` exist, `cluster-issuer` takes precedence. 145 | 146 | If `cert-manager.io/revision-history-limit` is present, it takes precedence over the value globally specified via the `--csr-revision-limit` command-line flag. 147 | 148 | [Contour]: https://github.com/projectcontour/contour 149 | [HTTPProxy]: https://projectcontour.io/docs/main/config/fundamentals/ 150 | [DNSEndpoint]: https://pkg.go.dev/github.com/kubernetes-sigs/external-dns/endpoint#DNSEndpoint 151 | [external-dns]: https://github.com/kubernetes-sigs/external-dns 152 | [Certificate]: https://cert-manager.io/docs/usage/certificate/ 153 | [cert-manager]: https://cert-manager.io/docs/ 154 | [Issuer]: https://cert-manager.io/docs/configuration/issuers/ 155 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [0.15.0] - 2025-12-08 9 | ### Changed 10 | - Add support for cert-manager key algorithm related annotations (#118) 11 | - feat: add annotation and label propagation to generated resources (#119) 12 | - add whitelisting of allowed custom delegation domains (#120) 13 | - remove id-kp-clientAuth EKU from certificate spec (#121) 14 | - docs: update dead/outdated links (#122) 15 | - feat: support the creation of child resources in isolated namespaces (#123) 16 | - refactor: avoid repetition by auto-generating description (#124) 17 | - Embed ReconcilerOptions (#125) 18 | - use goimports (#126) 19 | - chore: update dependencies for 2025/11 regular update (#127) 20 | 21 | ## [0.14.3] - 2025-07-22 22 | ### Changed 23 | - regular update 202507 (#116) 24 | 25 | ## [0.14.2] - 2025-03-28 26 | ### Changed 27 | - Update dependencies (#113) 28 | 29 | ## [0.14.1] - 2025-02-17 30 | ### Changed 31 | - Update for k8s 1.31 (#111) 32 | 33 | ## [0.14.0] - 2024-11-20 34 | ### Changed 35 | - Update contour to 1.30.1 (#109) 36 | - Kubernetes: 1.31 37 | - Contour: 1.30 38 | - ExternalDNS: 0.15 39 | - cert-manager: 1.16 40 | 41 | ## [0.13.0] - 2024-09-11 42 | ### Added 43 | - Add support for specifying revisionHistoryLimit for the generated Certificate (#104) 44 | - Add support for specifying revision history limit via annotation (#105) 45 | - Add support for using delegated domains for DNS-01 (#106) 46 | 47 | ### Changed 48 | - Update contour to 1.29.1 (#107) 49 | - Kubernetes: 1.30 50 | - Contour: 1.29 51 | - ExternalDNS: 0.15 52 | - cert-manager: 1.15 53 | 54 | ## [0.12.0] - 2024-04-18 55 | 56 | ### Changed 57 | - Update contour to 1.28.2 (#101) 58 | - Kubernetes: 1.29 59 | - Contour: 1.28 60 | - ExternalDNS: 0.14 61 | - cert-manager: 1.14 62 | 63 | ## [0.11.1] - 2024-01-15 64 | 65 | ### Breaking Changes 66 | 67 | #### Migrate image registry 68 | 69 | We migrated the image repository of contour-plus to `ghcr.io`. 70 | From contour-plus v0.11.0, please use the following image. 71 | 72 | - https://github.com/cybozu-go/contour-plus/pkgs/container/contour-plus 73 | 74 | The [quay.io/cybozu/contour-plus](https://quay.io/repository/cybozu/contour-plus) will not be updated in the future. 75 | 76 | ### Changed 77 | 78 | - Migrate to ghcr.io (#96) 79 | 80 | ## [0.10.0] - 2023-11-13 81 | 82 | ### Changed 83 | 84 | - Update contour to 1.27.0 and Kubernetes to 1.28 (#94) 85 | - Kubernetes: 1.28 86 | - Contour: 1.27 87 | - ExternalDNS: 0.13 88 | - cert-manager: 1.13 89 | 90 | ## [0.9.0] - 2023-04-27 91 | 92 | ### Changed 93 | 94 | - Support k8s 1.26 and update dependencies (#90) 95 | - Kubernetes: 1.26 96 | - Contour: 1.24 97 | - ExternalDNS: 0.13 98 | - cert-manager: 1.10 99 | 100 | ## [0.8.1] - 2022-11-02 101 | 102 | ### Changed 103 | 104 | - Update dependencies (#86) 105 | - Kubernetes: 1.25 106 | - Contour: 1.23 107 | 108 | 109 | ## [0.8.0] - 2022-09-30 110 | 111 | ### Changed 112 | 113 | - Update dependencies (#84) 114 | - Kubernetes: 1.24 115 | - Contour: 1.22 116 | - ExternalDNS: 0.12 117 | - cert-manager: 1.9 118 | - depended go packages and Actions 119 | 120 | 121 | ## [0.7.0] - 2022-04-12 122 | 123 | ### Changed 124 | 125 | - Update supported Kubernetes version to 1.23 (#78) 126 | - Kubernetes: 1.23 127 | - Contour: 1.20 128 | - ExternalDNS: 0.11 129 | - cert-manager: 1.7 130 | - Specify k8s version instead of using latest one during envtest (#77) 131 | - Update Makefile and Actions (#79) 132 | 133 | ## [0.6.6] - 2021-12-10 134 | 135 | ### Changed 136 | 137 | - update supported k8s version to 1.22 (#75) 138 | - kubernetes 1.22.1 139 | - contour 1.19.1 140 | - cert-manager 1.6.1 141 | - external-dns 0.10.1 142 | - Change LICENSE from MIT to Apache 2.0 (#73) 143 | 144 | ## [0.6.5] - 2021-09-17 145 | 146 | ## Changed 147 | - follow golang 1.17 and dependent software updates (#71) 148 | - golang 1.17 149 | - contour 1.18.1 150 | - cert-manager 1.5.3 151 | - external-dns 0.9.0 152 | 153 | ## [0.6.4] - 2021-08-03 154 | 155 | ### Changed 156 | - Update contour to 1.18.0 (#68) 157 | - Add support for the newly added HTTPProxy.Spec.IngressClassName 158 | 159 | ## [0.6.3] - 2021-07-27 160 | 161 | ### Changed 162 | - Update contour to 1.17.1 (#65) 163 | - Update controller-runtime to 0.9.3 (#65) 164 | 165 | ## [0.6.2] - 2021-04-20 166 | 167 | ### Changed 168 | 169 | - Fix the issue that options cannot be specified with environment variables. (#61) 170 | 171 | ## [0.6.1] - 2021-04-19 172 | 173 | ### Changed 174 | 175 | - Update contour to 1.14.1 (#59) 176 | - Update controller-runtime to 0.8.3 (#59) 177 | 178 | ## [0.6.0] - 2021-02-01 179 | 180 | ### Changed 181 | 182 | - Update contour to 1.11.0 (#53) 183 | - Update controller-runtime to 0.7.2 (#53) 184 | 185 | ## [0.5.2] - 2020-10-20 186 | 187 | ### Changed 188 | 189 | - Update contour to 1.9.0 (#48). 190 | - Use cert-manager v1 API Endpoint (#48). 191 | - Remove compile dependency on cert-manager and external-dns (#48). 192 | - Stop vendoring dependencies (#50). 193 | 194 | ## [0.5.1] - 2020-10-02 195 | 196 | ### Changed 197 | 198 | - Update controller-runtime to 0.6.3 (#46). 199 | 200 | ### Fixed 201 | 202 | - Do not reconcile being-deleted object (#46). 203 | 204 | ## [0.5.0] - 2020-06-30 205 | 206 | ### Changed 207 | 208 | - Update contour to 1.6.0, controller-runtime to 0.6.0 (#44). 209 | 210 | ### Removed 211 | 212 | - Support for IngressRoute has been discontinued (#44). 213 | 214 | ## [0.4.3] - 2020-05-08 215 | 216 | ### Changed 217 | 218 | - Add key usages to certificate resources to generate (#40). 219 | 220 | ## [0.4.2] - 2020-04-21 221 | 222 | ### Changed 223 | 224 | - Update contour to 1.3.0 and cert-manager to 0.14.1 (#35). 225 | - Update dependent packages for k8s v1.17.5 (#37). 226 | 227 | ## [0.4.1] - 2020-03-27 228 | 229 | ### Changed 230 | 231 | - Update dependent packages for k8s v1.17 (#33). 232 | 233 | ## [0.4.0] - 2019-12-13 234 | 235 | ### Added 236 | 237 | - Class-name filter (#28). 238 | 239 | ## [0.3.1] - 2019-12-04 240 | 241 | ### Changed 242 | 243 | - Fix missing initialize options of HTTPProxy controller (#25). 244 | 245 | ## [0.3.0] - 2019-11-13 246 | 247 | ### Changed 248 | 249 | - Update contour to 1.0.0, controller-runtime to 0.3.0, and cert-manager to 0.11.0 (#20). 250 | - Change the API version of Certificate resource from certmanger.k8s.io/v1alpha1 to cert-manger.io/v1alpha2 (#20). 251 | - Update controller-tools to 0.2.2 and kubebuilder to 2.1.0 (#21). 252 | 253 | ### Added 254 | 255 | - Support HTTPProxy resource (#20). 256 | 257 | ## [0.2.7] - 2019-08-26 258 | 259 | ### Changed 260 | 261 | - Add "list" verb for Services to the RBAC manifest (#18). 262 | - Update kubebuilder to 2.0.0 (#18). 263 | - Update controller-runtime and controller-tools to 0.2.0 (#18). 264 | 265 | ## [0.2.6] - 2019-06-12 266 | 267 | ### Changed 268 | 269 | - Update controller-runtime to v0.2.0-beta.2 (#12). 270 | 271 | ## [0.2.5] - 2019-06-06 272 | 273 | ### Changed 274 | 275 | - Enable leader election by default (#11). 276 | - Tidy up and fix bugs (#10). 277 | 278 | ## [0.2.4] - 2019-06-03 279 | 280 | ### Changed 281 | 282 | - Fixed resource name of Certificate in RBAC (#9). 283 | 284 | ## [0.2.3] - 2019-06-03 285 | 286 | ### Changed 287 | 288 | - Fixed null pointer exception bugs (#10). 289 | 290 | ## [0.2.2] - 2019-05-30 291 | 292 | ### Changed 293 | 294 | - Changed the default port of metrics server to :8180 (#8). 295 | - Do not mandate --service-name as it can be passed via CP_SERVICE_NAME envvar too (#8). 296 | 297 | ## [0.2.1] - 2019-05-29 298 | 299 | ### Changed 300 | 301 | - Initialize klog flags (#7). 302 | 303 | ## [0.2.0] - 2019-05-28 304 | 305 | ### Added 306 | 307 | - Leader election (#5). 308 | 309 | ## [0.1.0] - 2019-05-28 310 | 311 | ### Added 312 | 313 | - Implement `contour-plus` 314 | - with [kubebuilder][] v2.0.0-alpha.2 315 | - with [controller-runtime][] v0.2.0-beta.1 316 | - for [Contour][] v0.12.1 317 | - for [ExternalDNS][] v0.5.14 318 | - for [cert-manager][] v0.8.0 319 | 320 | [Unreleased]: https://github.com/cybozu-go/contour-plus/compare/v0.15.0...HEAD 321 | [0.14.3]: https://github.com/cybozu-go/contour-plus/compare/v0.14.3...v0.15.0 322 | [0.14.3]: https://github.com/cybozu-go/contour-plus/compare/v0.14.2...v0.14.3 323 | [0.14.2]: https://github.com/cybozu-go/contour-plus/compare/v0.14.1...v0.14.2 324 | [0.14.1]: https://github.com/cybozu-go/contour-plus/compare/v0.14.0...v0.14.1 325 | [0.14.0]: https://github.com/cybozu-go/contour-plus/compare/v0.13.0...v0.14.0 326 | [0.13.0]: https://github.com/cybozu-go/contour-plus/compare/v0.12.0...v0.13.0 327 | [0.12.0]: https://github.com/cybozu-go/contour-plus/compare/v0.11.1...v0.12.0 328 | [0.11.1]: https://github.com/cybozu-go/contour-plus/compare/v0.10.0...v0.11.1 329 | [0.10.0]: https://github.com/cybozu-go/contour-plus/compare/v0.9.0...v0.10.0 330 | [0.9.0]: https://github.com/cybozu-go/contour-plus/compare/v0.8.1...v0.9.0 331 | [0.8.1]: https://github.com/cybozu-go/contour-plus/compare/v0.8.0...v0.8.1 332 | [0.8.0]: https://github.com/cybozu-go/contour-plus/compare/v0.7.0...v0.8.0 333 | [0.7.0]: https://github.com/cybozu-go/contour-plus/compare/v0.6.6...v0.7.0 334 | [0.6.6]: https://github.com/cybozu-go/contour-plus/compare/v0.6.5...v0.6.6 335 | [0.6.5]: https://github.com/cybozu-go/contour-plus/compare/v0.6.4...v0.6.5 336 | [0.6.4]: https://github.com/cybozu-go/contour-plus/compare/v0.6.3...v0.6.4 337 | [0.6.3]: https://github.com/cybozu-go/contour-plus/compare/v0.6.2...v0.6.3 338 | [0.6.2]: https://github.com/cybozu-go/contour-plus/compare/v0.6.1...v0.6.2 339 | [0.6.1]: https://github.com/cybozu-go/contour-plus/compare/v0.6.0...v0.6.1 340 | [0.6.0]: https://github.com/cybozu-go/contour-plus/compare/v0.5.2...v0.6.0 341 | [0.5.2]: https://github.com/cybozu-go/contour-plus/compare/v0.5.1...v0.5.2 342 | [0.5.1]: https://github.com/cybozu-go/contour-plus/compare/v0.5.0...v0.5.1 343 | [0.5.0]: https://github.com/cybozu-go/contour-plus/compare/v0.4.3...v0.5.0 344 | [0.4.3]: https://github.com/cybozu-go/contour-plus/compare/v0.4.2...v0.4.3 345 | [0.4.2]: https://github.com/cybozu-go/contour-plus/compare/v0.4.1...v0.4.2 346 | [0.4.1]: https://github.com/cybozu-go/contour-plus/compare/v0.4.0...v0.4.1 347 | [0.4.0]: https://github.com/cybozu-go/contour-plus/compare/v0.3.1...v0.4.0 348 | [0.3.1]: https://github.com/cybozu-go/contour-plus/compare/v0.3.0...v0.3.1 349 | [0.3.0]: https://github.com/cybozu-go/contour-plus/compare/v0.2.7...v0.3.0 350 | [0.2.7]: https://github.com/cybozu-go/contour-plus/compare/v0.2.6...v0.2.7 351 | [0.2.6]: https://github.com/cybozu-go/contour-plus/compare/v0.2.5...v0.2.6 352 | [0.2.5]: https://github.com/cybozu-go/contour-plus/compare/v0.2.4...v0.2.5 353 | [0.2.4]: https://github.com/cybozu-go/contour-plus/compare/v0.2.3...v0.2.4 354 | [0.2.3]: https://github.com/cybozu-go/contour-plus/compare/v0.2.2...v0.2.3 355 | [0.2.2]: https://github.com/cybozu-go/contour-plus/compare/v0.2.1...v0.2.2 356 | [0.2.1]: https://github.com/cybozu-go/contour-plus/compare/v0.2.0...v0.2.1 357 | [0.2.0]: https://github.com/cybozu-go/contour-plus/compare/v0.1.0...v0.2.0 358 | [0.1.0]: https://github.com/cybozu-go/contour-plus/compare/e51fdf92f56eaf3e9eb4b3cce6527dc6d97626e3...v0.1.0 359 | [kubebuilder]: https://github.com/kubernetes-sigs/kubebuilder 360 | [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime 361 | [Contour]: https://github.com/heptio/contour 362 | [ExternalDNS]: https://github.com/kubernetes-sigs/external-dns 363 | [cert-manager]: https://github.com/jetstack/cert-manager/tree/v0.8.0 364 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 2 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= 13 | github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 14 | github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= 15 | github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 16 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 17 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 18 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 19 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 20 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 21 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 22 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 23 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 24 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 25 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 26 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 27 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 28 | github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= 29 | github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= 30 | github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= 31 | github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= 32 | github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= 33 | github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= 34 | github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= 35 | github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= 36 | github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= 37 | github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= 38 | github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= 39 | github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= 40 | github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= 41 | github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= 42 | github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= 43 | github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= 44 | github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= 45 | github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= 46 | github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= 47 | github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= 48 | github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= 49 | github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= 50 | github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= 51 | github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= 52 | github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= 53 | github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= 54 | github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= 55 | github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= 56 | github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= 57 | github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= 58 | github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= 59 | github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= 60 | github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= 61 | github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 62 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 63 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 64 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 65 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 66 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 67 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 68 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 69 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 70 | github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= 71 | github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 72 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 73 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 74 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 76 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 77 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 78 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 79 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 80 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 82 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 83 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 84 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 85 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 86 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 87 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 88 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 89 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 90 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 91 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 92 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 93 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 94 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 95 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 96 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 97 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 98 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 99 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 100 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 101 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 102 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 103 | github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= 104 | github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= 105 | github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 106 | github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 107 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 108 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 109 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 110 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 111 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 112 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 113 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 114 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 115 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 116 | github.com/projectcontour/contour v1.33.0 h1:Aewh+Yt5DY005CGvdxtpquZWmc6/6IsDTs1ze8CvtZk= 117 | github.com/projectcontour/contour v1.33.0/go.mod h1:bCNCQICmheYMj1kx4dEWHp7fXKZc3nbHbWj1tqZ77rc= 118 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 119 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 120 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 121 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 122 | github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= 123 | github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 124 | github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= 125 | github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 126 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 127 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 128 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 129 | github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= 130 | github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= 131 | github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= 132 | github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= 133 | github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= 134 | github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 135 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 136 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 137 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 138 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 139 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 140 | github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= 141 | github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 144 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 145 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 146 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 147 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 148 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 149 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 150 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 151 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 152 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 153 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 154 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 155 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 156 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 157 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 158 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 159 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 160 | go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 161 | go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 162 | go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 163 | go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 164 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 165 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 166 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 167 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 168 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 169 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 170 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 175 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 176 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 177 | golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= 178 | golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 179 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 183 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 184 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 185 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 188 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 189 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 190 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 191 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 192 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 193 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 194 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 195 | golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 196 | golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 197 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 198 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 199 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 200 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 201 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 202 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 203 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 204 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 206 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 207 | gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= 208 | gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 209 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 210 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 211 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 212 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 213 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 214 | gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 215 | gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 216 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 217 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 218 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 219 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 220 | k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= 221 | k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= 222 | k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= 223 | k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= 224 | k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= 225 | k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 226 | k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= 227 | k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= 228 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 229 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 230 | k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= 231 | k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 232 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 233 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 234 | sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= 235 | sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= 236 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 237 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 238 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 239 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 240 | sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= 241 | sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 242 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 243 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 244 | -------------------------------------------------------------------------------- /controllers/httpproxy_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "slices" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/go-logr/logr" 11 | projectcontourv1 "github.com/projectcontour/contour/apis/projectcontour/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/types" 17 | "k8s.io/utils/ptr" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 21 | "sigs.k8s.io/controller-runtime/pkg/handler" 22 | crlog "sigs.k8s.io/controller-runtime/pkg/log" 23 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 24 | ) 25 | 26 | const ( 27 | excludeAnnotation = "contour-plus.cybozu.com/exclude" 28 | testACMETLSAnnotation = "kubernetes.io/tls-acme" 29 | issuerNameAnnotation = "cert-manager.io/issuer" 30 | clusterIssuerNameAnnotation = "cert-manager.io/cluster-issuer" 31 | revisionHistoryLimitAnnotation = "cert-manager.io/revision-history-limit" 32 | privateKeyAlgorithmAnnotation = "cert-manager.io/private-key-algorithm" 33 | privateKeySizeAnnotation = "cert-manager.io/private-key-size" 34 | ingressClassNameAnnotation = "kubernetes.io/ingress.class" 35 | contourIngressClassNameAnnotation = "projectcontour.io/ingress.class" 36 | delegatedDomainAnnotation = "contour-plus.cybozu.com/delegated-domain" 37 | dnsNamespaceAnnotation = "contour-plus.cybozu.com/dns-namespace" 38 | issuerNamespaceAnnotation = "contour-plus.cybozu.com/issuer-namespace" 39 | crossNamespaceOwnerAnnotation = "contour-plus.cybozu.com/owned-by" 40 | finalizerName = "contour-plus.cybozu.com/finalizer" 41 | ) 42 | 43 | // HTTPProxyReconciler reconciles a HTTPProxy object 44 | type HTTPProxyReconciler struct { 45 | client.Client 46 | ReconcilerOptions 47 | Log logr.Logger 48 | Scheme *runtime.Scheme 49 | } 50 | 51 | // +kubebuilder:rbac:groups=projectcontour.io,resources=httpproxies,verbs=get;list;watch;update;patch 52 | // +kubebuilder:rbac:groups=projectcontour.io,resources=httpproxies/status,verbs=get 53 | // +kubebuilder:rbac:groups=projectcontour.io.resources=tlscertificatedelegations,verbs=get;list;watch;create;update;patch;delete 54 | // +kubebuilder:rbac:groups=externaldns.k8s.io,resources=dnsendpoints,verbs=get;list;watch;create;update;patch;delete 55 | // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete 56 | // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch 57 | // +kubebuilder:rbac:groups="",resources=services/status,verbs=get 58 | 59 | // Reconcile creates/updates CRDs from given HTTPProxy 60 | func (r *HTTPProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 61 | log := crlog.FromContext(ctx) 62 | 63 | // Get HTTPProxy 64 | hp := new(projectcontourv1.HTTPProxy) 65 | objKey := client.ObjectKey{ 66 | Namespace: req.Namespace, 67 | Name: req.Name, 68 | } 69 | err := r.Get(ctx, objKey, hp) 70 | if k8serrors.IsNotFound(err) { 71 | return ctrl.Result{}, nil 72 | } 73 | if err != nil { 74 | log.Error(err, "unable to get HTTPProxy resources") 75 | return ctrl.Result{}, err 76 | } 77 | 78 | if hp.DeletionTimestamp != nil { 79 | if !controllerutil.ContainsFinalizer(hp, finalizerName) { 80 | return ctrl.Result{}, nil 81 | } 82 | // Clean up owned resources in other namespaces 83 | return ctrl.Result{}, r.cleanupCrossNamespaceResources(ctx, hp, log) 84 | } 85 | 86 | if hp.Annotations[excludeAnnotation] == "true" { 87 | return ctrl.Result{}, nil 88 | } 89 | 90 | if r.IngressClassName != "" { 91 | if !r.isClassNameMatched(hp) { 92 | return ctrl.Result{}, nil 93 | } 94 | } 95 | 96 | if err := r.reconcileDNSEndpoint(ctx, hp, log); err != nil { 97 | log.Error(err, "unable to reconcile DNSEndpoint") 98 | return ctrl.Result{}, err 99 | } 100 | 101 | if err := r.reconcileDelegationDNSEndpoint(ctx, hp, log); err != nil { 102 | log.Error(err, "unable to reconcile delegation DNSEndpoint") 103 | return ctrl.Result{}, err 104 | } 105 | 106 | if err := r.reconcileCertificate(ctx, hp, log); err != nil { 107 | log.Error(err, "unable to reconcile Certificate") 108 | return ctrl.Result{}, err 109 | } 110 | 111 | if err := r.reconcileTLSCertificateDelegation(ctx, hp, log); err != nil { 112 | log.Error(err, "unable to reconcile TLSCertificateDelegation") 113 | return ctrl.Result{}, err 114 | } 115 | 116 | if err := r.reconcileSecretName(ctx, hp, log); err != nil { 117 | log.Error(err, "unable to reconcile HTTPProxy SecretName") 118 | return ctrl.Result{}, err 119 | } 120 | return ctrl.Result{}, nil 121 | } 122 | 123 | func (r *HTTPProxyReconciler) isClassNameMatched(hp *projectcontourv1.HTTPProxy) bool { 124 | ingressClassName := hp.Annotations[ingressClassNameAnnotation] 125 | if ingressClassName != "" { 126 | if ingressClassName != r.IngressClassName { 127 | return false 128 | } 129 | } 130 | 131 | contourIngressClassName := hp.Annotations[contourIngressClassNameAnnotation] 132 | if contourIngressClassName != "" { 133 | if contourIngressClassName != r.IngressClassName { 134 | return false 135 | } 136 | } 137 | 138 | specIngressClassName := hp.Spec.IngressClassName 139 | if specIngressClassName != "" { 140 | if specIngressClassName != r.IngressClassName { 141 | return false 142 | } 143 | } 144 | 145 | if contourIngressClassName == "" && ingressClassName == "" && specIngressClassName == "" { 146 | return false 147 | } 148 | 149 | return true 150 | } 151 | 152 | func (r *HTTPProxyReconciler) reconcileDNSEndpoint(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 153 | if !r.CreateDNSEndpoint { 154 | return nil 155 | } 156 | 157 | if hp.Spec.VirtualHost == nil { 158 | return nil 159 | } 160 | fqdn := hp.Spec.VirtualHost.Fqdn 161 | if len(fqdn) == 0 { 162 | return nil 163 | } 164 | 165 | // Get IP list of loadbalancer Service 166 | var serviceIPs []net.IP 167 | var svc corev1.Service 168 | err := r.Get(ctx, r.ServiceKey, &svc) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | for _, ing := range svc.Status.LoadBalancer.Ingress { 174 | if len(ing.IP) == 0 { 175 | continue 176 | } 177 | serviceIPs = append(serviceIPs, net.ParseIP(ing.IP)) 178 | } 179 | if len(serviceIPs) == 0 { 180 | log.Info("no IP address for service " + r.ServiceKey.String()) 181 | // we can return nil here because the controller will be notified 182 | // as soon as a new IP address is assigned to the service. 183 | return nil 184 | } 185 | 186 | dnsEndpointName := getDNSEndpointName(r, hp) 187 | targetNamespace := hp.Namespace 188 | if ns, ok := hp.Annotations[dnsNamespaceAnnotation]; ok && slices.Contains(r.AllowedDNSNamespaces, ns) { 189 | targetNamespace = ns 190 | } 191 | 192 | obj := &unstructured.Unstructured{} 193 | obj.SetGroupVersionKind(externalDNSGroupVersion.WithKind(DNSEndpointKind)) 194 | obj.SetName(dnsEndpointName) 195 | obj.SetNamespace(targetNamespace) 196 | obj.SetAnnotations(r.generateObjectAnnotations(hp)) 197 | obj.SetLabels(r.generateObjectLabels(hp)) 198 | obj.UnstructuredContent()["spec"] = map[string]interface{}{ 199 | "endpoints": makeEndpoints(fqdn, serviceIPs), 200 | } 201 | err = r.trackResourceForCleanup(hp, obj) 202 | if err != nil { 203 | return err 204 | } 205 | err = r.Patch(ctx, obj, client.Apply, &client.PatchOptions{ 206 | Force: ptr.To(true), 207 | FieldManager: "contour-plus", 208 | }) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | log.Info("DNSEndpoint successfully reconciled") 214 | return nil 215 | } 216 | 217 | func (r *HTTPProxyReconciler) reconcileDelegationDNSEndpoint(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 218 | if !r.CreateDNSEndpoint { 219 | return nil 220 | } 221 | 222 | delegatedDomain := r.DefaultDelegatedDomain 223 | userDelegatedDomain := hp.Annotations[delegatedDomainAnnotation] 224 | if userDelegatedDomain != "" && r.AllowCustomDelegations && slices.Contains(r.AllowedDelegatedDomains, userDelegatedDomain) { 225 | delegatedDomain = userDelegatedDomain 226 | } 227 | 228 | if delegatedDomain == "" { 229 | return nil 230 | } 231 | 232 | if hp.Spec.VirtualHost == nil { 233 | return nil 234 | } 235 | fqdn := hp.Spec.VirtualHost.Fqdn 236 | if len(fqdn) == 0 { 237 | return nil 238 | } 239 | 240 | dnsEndpointName := getDNSEndpointName(r, hp) 241 | targetNamespace := hp.Namespace 242 | if ns, ok := hp.Annotations[dnsNamespaceAnnotation]; ok && slices.Contains(r.AllowedDNSNamespaces, ns) { 243 | targetNamespace = ns 244 | } 245 | 246 | obj := &unstructured.Unstructured{} 247 | obj.SetGroupVersionKind(externalDNSGroupVersion.WithKind(DNSEndpointKind)) 248 | obj.SetName(dnsEndpointName + "-delegation") 249 | obj.SetNamespace(targetNamespace) 250 | obj.SetAnnotations(r.generateObjectAnnotations(hp)) 251 | obj.SetLabels(r.generateObjectLabels(hp)) 252 | obj.UnstructuredContent()["spec"] = map[string]interface{}{ 253 | "endpoints": makeDelegationEndpoint(fqdn, delegatedDomain), 254 | } 255 | 256 | if err := r.trackResourceForCleanup(hp, obj); err != nil { 257 | return err 258 | } 259 | 260 | if err := r.Patch(ctx, obj, client.Apply, &client.PatchOptions{ 261 | Force: ptr.To(true), 262 | FieldManager: "contour-plus", 263 | }); err != nil { 264 | return err 265 | } 266 | 267 | log.Info("Delegation DNSEndpoint successfully reconciled") 268 | return nil 269 | } 270 | 271 | func (r *HTTPProxyReconciler) reconcileCertificate(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 272 | if !r.CreateCertificate { 273 | return nil 274 | } 275 | if hp.Annotations[testACMETLSAnnotation] != "true" { 276 | return nil 277 | } 278 | 279 | vh := hp.Spec.VirtualHost 280 | switch { 281 | case vh == nil: 282 | return nil 283 | case vh.Fqdn == "": 284 | return nil 285 | } 286 | secretName := getCertificateSecretName(r, hp) 287 | if secretName == "" { 288 | return nil 289 | } 290 | 291 | issuerName := r.DefaultIssuerName 292 | issuerKind := r.DefaultIssuerKind 293 | if name, ok := hp.Annotations[issuerNameAnnotation]; ok { 294 | issuerName = name 295 | issuerKind = IssuerKind 296 | } 297 | if name, ok := hp.Annotations[clusterIssuerNameAnnotation]; ok { 298 | issuerName = name 299 | issuerKind = ClusterIssuerKind 300 | } 301 | 302 | if issuerName == "" { 303 | log.Info("no issuer name") 304 | return nil 305 | } 306 | 307 | certificateSpec := map[string]interface{}{ 308 | "dnsNames": []string{vh.Fqdn}, 309 | "secretName": secretName, 310 | "commonName": vh.Fqdn, 311 | "issuerRef": map[string]interface{}{ 312 | "kind": issuerKind, 313 | "name": issuerName, 314 | }, 315 | "usages": []string{ 316 | usageDigitalSignature, 317 | usageKeyEncipherment, 318 | usageServerAuth, 319 | }, 320 | } 321 | 322 | if r.CSRRevisionLimit > 0 { 323 | certificateSpec["revisionHistoryLimit"] = r.CSRRevisionLimit 324 | } 325 | if value, ok := hp.Annotations[revisionHistoryLimitAnnotation]; ok { 326 | limit, err := strconv.ParseUint(value, 10, 32) 327 | if err != nil { 328 | log.Error(err, "invalid revisionHistoryLimit", "value", value) 329 | return nil 330 | } 331 | certificateSpec["revisionHistoryLimit"] = limit 332 | } 333 | annotations := r.generateObjectAnnotations(hp) 334 | labels := r.generateObjectLabels(hp) 335 | secretTemplate := map[string]interface{}{ 336 | "annotations": annotations, 337 | "labels": labels, 338 | } 339 | certificateSpec["secretTemplate"] = secretTemplate 340 | 341 | if algorithm, ok := hp.Annotations[privateKeyAlgorithmAnnotation]; ok { 342 | privateKeySpec := map[string]interface{}{ 343 | "algorithm": algorithm, 344 | } 345 | if value, ok := hp.Annotations[privateKeySizeAnnotation]; ok { 346 | size, err := strconv.ParseUint(value, 10, 32) 347 | if err == nil { 348 | privateKeySpec["size"] = size 349 | } else { 350 | log.Error(err, "invalid privateKey size", "value", value) 351 | } 352 | } 353 | certificateSpec["privateKey"] = privateKeySpec 354 | } 355 | 356 | certificateName := getCertificateName(r, hp) 357 | targetNamespace := hp.Namespace 358 | if ns, ok := hp.Annotations[issuerNamespaceAnnotation]; ok && slices.Contains(r.AllowedIssuerNamespaces, ns) { 359 | targetNamespace = ns 360 | } 361 | 362 | obj := &unstructured.Unstructured{} 363 | obj.SetGroupVersionKind(certManagerGroupVersion.WithKind(CertificateKind)) 364 | obj.SetName(certificateName) 365 | obj.SetNamespace(targetNamespace) 366 | obj.UnstructuredContent()["spec"] = certificateSpec 367 | 368 | obj.SetAnnotations(annotations) 369 | obj.SetLabels(labels) 370 | 371 | err := r.trackResourceForCleanup(hp, obj) 372 | if err != nil { 373 | return err 374 | } 375 | err = r.Patch(ctx, obj, client.Apply, &client.PatchOptions{ 376 | Force: ptr.To(true), 377 | FieldManager: "contour-plus", 378 | }) 379 | if err != nil { 380 | return err 381 | } 382 | 383 | log.Info("Certificate successfully reconciled") 384 | return nil 385 | } 386 | 387 | func (r *HTTPProxyReconciler) generateObjectAnnotations(hp *projectcontourv1.HTTPProxy) map[string]string { 388 | annotations := map[string]string{} 389 | for _, key := range r.PropagatedAnnotations { 390 | if annotation, ok := hp.Annotations[key]; ok { 391 | annotations[key] = annotation 392 | } 393 | } 394 | return annotations 395 | } 396 | 397 | func (r *HTTPProxyReconciler) generateObjectLabels(hp *projectcontourv1.HTTPProxy) map[string]string { 398 | labels := map[string]string{} 399 | for _, key := range r.PropagatedLabels { 400 | if label, ok := hp.Labels[key]; ok { 401 | labels[key] = label 402 | } 403 | } 404 | return labels 405 | } 406 | 407 | func (r *HTTPProxyReconciler) reconcileTLSCertificateDelegation(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 408 | namespace, ok := hp.Annotations[issuerNamespaceAnnotation] 409 | if !ok || !slices.Contains(r.AllowedIssuerNamespaces, namespace) { 410 | return nil 411 | } 412 | certificateName := getCertificateName(r, hp) 413 | 414 | cert := &unstructured.Unstructured{} 415 | cert.SetGroupVersionKind(certManagerGroupVersion.WithKind(CertificateKind)) 416 | certKey := client.ObjectKey{ 417 | Namespace: namespace, 418 | Name: certificateName, 419 | } 420 | err := r.Get(ctx, certKey, cert) 421 | if k8serrors.IsNotFound(err) { 422 | log.Info("Certificate not found for TLSCertificateDelegation", "namespace", namespace, "name", certificateName) 423 | return err 424 | } 425 | 426 | delegationSpec := map[string]interface{}{ 427 | "delegations": []map[string]interface{}{ 428 | { 429 | "secretName": certificateName, 430 | "targetNamespaces": []string{ 431 | hp.Namespace, 432 | }, 433 | }, 434 | }, 435 | } 436 | 437 | obj := &unstructured.Unstructured{} 438 | obj.SetGroupVersionKind(contourGroupVersion.WithKind(TLSCertificateDelegationKind)) 439 | obj.SetName(certificateName) 440 | obj.SetNamespace(namespace) 441 | obj.SetAnnotations(r.generateObjectAnnotations(hp)) 442 | obj.SetLabels(r.generateObjectLabels(hp)) 443 | obj.UnstructuredContent()["spec"] = delegationSpec 444 | err = r.trackResourceForCleanup(hp, obj) 445 | if err != nil { 446 | return err 447 | } 448 | err = r.Patch(ctx, obj, client.Apply, &client.PatchOptions{ 449 | Force: ptr.To(true), 450 | FieldManager: "contour-plus", 451 | }) 452 | if err != nil { 453 | return err 454 | } 455 | 456 | log.Info("TLSCertificateDelegation successfully reconciled") 457 | return nil 458 | } 459 | 460 | func (r *HTTPProxyReconciler) reconcileSecretName(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 461 | certNamespace, ok := hp.Annotations[issuerNamespaceAnnotation] 462 | if !ok || !slices.Contains(r.AllowedIssuerNamespaces, certNamespace) { 463 | return nil 464 | } 465 | certificateName := getCertificateName(r, hp) 466 | if hp.Spec.VirtualHost.TLS == nil { 467 | hp.Spec.VirtualHost.TLS = &projectcontourv1.TLS{} 468 | } 469 | hp.Spec.VirtualHost.TLS.SecretName = certNamespace + "/" + certificateName 470 | 471 | err := r.Patch(ctx, hp, client.Merge) 472 | if err != nil { 473 | return err 474 | } 475 | 476 | log.Info("HTTPProxy SecretName successfully reconciled") 477 | return nil 478 | } 479 | 480 | func (r *HTTPProxyReconciler) trackResourceForCleanup(hp *projectcontourv1.HTTPProxy, obj *unstructured.Unstructured) error { 481 | if obj.GetNamespace() == hp.Namespace { 482 | return ctrl.SetControllerReference(hp, obj, r.Scheme) 483 | } 484 | 485 | annotations := obj.GetAnnotations() 486 | if annotations == nil { 487 | annotations = make(map[string]string) 488 | } 489 | annotations[crossNamespaceOwnerAnnotation] = hp.Namespace + "/" + hp.Name 490 | obj.SetAnnotations(annotations) 491 | 492 | if !controllerutil.ContainsFinalizer(hp, finalizerName) { 493 | controllerutil.AddFinalizer(hp, finalizerName) 494 | err := r.Update(context.Background(), hp) 495 | return err 496 | } 497 | return nil 498 | } 499 | 500 | func (r *HTTPProxyReconciler) cleanupCrossNamespaceResources(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 501 | if !controllerutil.ContainsFinalizer(hp, finalizerName) { 502 | return nil 503 | } 504 | 505 | if err := r.cleanupCrossNamespaceDNSEndpoints(ctx, hp, log); err != nil { 506 | return err 507 | } 508 | 509 | if err := r.cleanupCrossNamespaceCertificates(ctx, hp, log); err != nil { 510 | return err 511 | } 512 | 513 | if err := r.cleanupCrossNamespaceTLSCertificateDelegations(ctx, hp, log); err != nil { 514 | return err 515 | } 516 | 517 | controllerutil.RemoveFinalizer(hp, finalizerName) 518 | return r.Update(ctx, hp) 519 | } 520 | 521 | func (r *HTTPProxyReconciler) cleanupCrossNamespaceDNSEndpoints(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 522 | if !r.CreateDNSEndpoint { 523 | return nil 524 | } 525 | 526 | deNs, ok := hp.Annotations[dnsNamespaceAnnotation] 527 | if !ok || !slices.Contains(r.AllowedDNSNamespaces, deNs) { 528 | return nil 529 | } 530 | 531 | del := &unstructured.UnstructuredList{} 532 | del.SetGroupVersionKind(externalDNSGroupVersion.WithKind(DNSEndpointKind)) 533 | err := r.List(ctx, del, &client.ListOptions{Namespace: deNs}) 534 | if err != nil { 535 | return err 536 | } 537 | 538 | for _, de := range del.Items { 539 | annotations := de.GetAnnotations() 540 | owner, ok := annotations[crossNamespaceOwnerAnnotation] 541 | if !ok || owner != hp.Namespace+"/"+hp.Name { 542 | continue 543 | } 544 | 545 | err := r.Delete(ctx, &de) 546 | if err != nil && !k8serrors.IsNotFound(err) { 547 | log.Error(err, "failed to delete cross-namespace DNSEndpoint", "name", de.GetName(), "namespace", de.GetNamespace()) 548 | return err 549 | } 550 | log.Info("deleted cross-namespace DNSEndpoint", "name", de.GetName(), "namespace", de.GetNamespace()) 551 | } 552 | return nil 553 | } 554 | 555 | func (r *HTTPProxyReconciler) cleanupCrossNamespaceCertificates(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 556 | if !r.CreateCertificate { 557 | return nil 558 | } 559 | 560 | issuerNs, ok := hp.Annotations[issuerNamespaceAnnotation] 561 | if !ok || !slices.Contains(r.AllowedIssuerNamespaces, issuerNs) { 562 | return nil 563 | } 564 | 565 | certList := &unstructured.UnstructuredList{} 566 | certList.SetGroupVersionKind(certManagerGroupVersion.WithKind(CertificateKind)) 567 | err := r.List(ctx, certList, &client.ListOptions{Namespace: issuerNs}) 568 | if err != nil { 569 | return err 570 | } 571 | 572 | for _, cert := range certList.Items { 573 | annotations := cert.GetAnnotations() 574 | owner, ok := annotations[crossNamespaceOwnerAnnotation] 575 | if !ok || owner != hp.Namespace+"/"+hp.Name { 576 | continue 577 | } 578 | 579 | err := r.Delete(ctx, &cert) 580 | if err != nil && !k8serrors.IsNotFound(err) { 581 | log.Error(err, "failed to delete cross-namespace Certificate", "name", cert.GetName(), "namespace", cert.GetNamespace()) 582 | return err 583 | } 584 | log.Info("deleted cross-namespace Certificate", "name", cert.GetName(), "namespace", cert.GetNamespace()) 585 | } 586 | return nil 587 | } 588 | 589 | func (r *HTTPProxyReconciler) cleanupCrossNamespaceTLSCertificateDelegations(ctx context.Context, hp *projectcontourv1.HTTPProxy, log logr.Logger) error { 590 | if !r.CreateCertificate { 591 | return nil 592 | } 593 | 594 | issuerNs, ok := hp.Annotations[issuerNamespaceAnnotation] 595 | if !ok || !slices.Contains(r.AllowedIssuerNamespaces, issuerNs) { 596 | return nil 597 | } 598 | 599 | tcdList := &unstructured.UnstructuredList{} 600 | tcdList.SetGroupVersionKind(contourGroupVersion.WithKind(TLSCertificateDelegationListKind)) 601 | err := r.List(ctx, tcdList, &client.ListOptions{Namespace: issuerNs}) 602 | if err != nil { 603 | return err 604 | } 605 | 606 | for _, tcd := range tcdList.Items { 607 | annotations := tcd.GetAnnotations() 608 | owner, ok := annotations[crossNamespaceOwnerAnnotation] 609 | if !ok || owner != hp.Namespace+"/"+hp.Name { 610 | continue 611 | } 612 | 613 | err := r.Delete(ctx, &tcd) 614 | if err != nil && !k8serrors.IsNotFound(err) { 615 | log.Error(err, "failed to delete cross-namespace TLSCertificateDelegation", "name", tcd.GetName(), "namespace", tcd.GetNamespace()) 616 | return err 617 | } 618 | log.Info("deleted cross-namespace TLSCertificateDelegation", "name", tcd.GetName(), "namespace", tcd.GetNamespace()) 619 | } 620 | return nil 621 | } 622 | 623 | // SetupWithManager sets up the controller with the Manager. 624 | func (r *HTTPProxyReconciler) SetupWithManager(mgr ctrl.Manager) error { 625 | listHPs := func(ctx context.Context, a client.Object) []reconcile.Request { 626 | if a.GetNamespace() != r.ServiceKey.Namespace { 627 | return nil 628 | } 629 | if a.GetName() != r.ServiceKey.Name { 630 | return nil 631 | } 632 | 633 | var hpList projectcontourv1.HTTPProxyList 634 | err := r.List(ctx, &hpList) 635 | if err != nil { 636 | r.Log.Error(err, "listing HTTPProxy failed") 637 | return nil 638 | } 639 | 640 | requests := make([]reconcile.Request, len(hpList.Items)) 641 | for i, hp := range hpList.Items { 642 | requests[i] = reconcile.Request{NamespacedName: types.NamespacedName{ 643 | Name: hp.Name, 644 | Namespace: hp.Namespace, 645 | }} 646 | } 647 | return requests 648 | } 649 | 650 | b := ctrl.NewControllerManagedBy(mgr). 651 | For(&projectcontourv1.HTTPProxy{}). 652 | Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(listHPs)) 653 | if r.CreateDNSEndpoint { 654 | obj := &unstructured.Unstructured{} 655 | obj.SetGroupVersionKind(externalDNSGroupVersion.WithKind(DNSEndpointKind)) 656 | b = b.Owns(obj) 657 | } 658 | if r.CreateCertificate { 659 | obj := &unstructured.Unstructured{} 660 | obj.SetGroupVersionKind(certManagerGroupVersion.WithKind(CertificateKind)) 661 | b = b.Owns(obj) 662 | tcdObj := &unstructured.Unstructured{} 663 | tcdObj.SetGroupVersionKind(contourGroupVersion.WithKind(TLSCertificateDelegationKind)) 664 | b = b.Owns(tcdObj) 665 | } 666 | return b.Complete(r) 667 | } 668 | 669 | func makeEndpoints(hostname string, ips []net.IP) []map[string]interface{} { 670 | ipv4Targets, ipv6Targets := ipsToTargets(ips) 671 | var endpoints []map[string]interface{} 672 | if len(ipv4Targets) != 0 { 673 | endpoints = append(endpoints, map[string]interface{}{ 674 | "dnsName": hostname, 675 | "targets": ipv4Targets, 676 | "recordType": "A", 677 | "recordTTL": 3600, 678 | }) 679 | } 680 | if len(ipv6Targets) != 0 { 681 | endpoints = append(endpoints, map[string]interface{}{ 682 | "dnsName": hostname, 683 | "targets": ipv6Targets, 684 | "recordType": "AAAA", 685 | "recordTTL": 3600, 686 | }) 687 | } 688 | return endpoints 689 | } 690 | 691 | func ipsToTargets(ips []net.IP) ([]string, []string) { 692 | var ipv4Targets []string 693 | var ipv6Targets []string 694 | for _, ip := range ips { 695 | if ip.To4() != nil { 696 | ipv4Targets = append(ipv4Targets, ip.String()) 697 | continue 698 | } 699 | ipv6Targets = append(ipv6Targets, ip.String()) 700 | } 701 | return ipv4Targets, ipv6Targets 702 | } 703 | 704 | func makeDelegationEndpoint(hostname, delegatedDomain string) []map[string]interface{} { 705 | fqdn := strings.Trim(hostname, ".") 706 | return []map[string]interface{}{ 707 | { 708 | "dnsName": "_acme-challenge." + fqdn, 709 | "targets": []string{"_acme-challenge." + fqdn + "." + delegatedDomain}, 710 | "recordType": "CNAME", 711 | "recordTTL": 3600, 712 | }, 713 | } 714 | } 715 | 716 | func getCertificateName(r *HTTPProxyReconciler, hp *projectcontourv1.HTTPProxy) string { 717 | certNamespace, ok := hp.Annotations[issuerNamespaceAnnotation] 718 | if !ok || certNamespace == "" || certNamespace == hp.Namespace { 719 | return r.Prefix + hp.Name 720 | } 721 | return r.Prefix + hp.Namespace + "-" + hp.Name 722 | } 723 | 724 | func getCertificateSecretName(r *HTTPProxyReconciler, hp *projectcontourv1.HTTPProxy) string { 725 | certNamespace, ok := hp.Annotations[issuerNamespaceAnnotation] 726 | if !ok || certNamespace == "" || certNamespace == hp.Namespace { 727 | if hp.Spec.VirtualHost.TLS == nil { 728 | return "" 729 | } 730 | return hp.Spec.VirtualHost.TLS.SecretName 731 | } 732 | return r.Prefix + hp.Namespace + "-" + hp.Name 733 | } 734 | 735 | func getDNSEndpointName(r *HTTPProxyReconciler, hp *projectcontourv1.HTTPProxy) string { 736 | deNamespace, ok := hp.Annotations[dnsNamespaceAnnotation] 737 | if !ok || deNamespace == "" || deNamespace == hp.Namespace { 738 | return r.Prefix + hp.Name 739 | } 740 | return r.Prefix + hp.Namespace + "-" + hp.Name 741 | } 742 | -------------------------------------------------------------------------------- /controllers/httpproxy_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | projectcontourv1 "github.com/projectcontour/contour/apis/projectcontour/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | ) 18 | 19 | const ( 20 | dnsName = "test.example.com" 21 | testSecretName = "test-secret" 22 | testDelegationName = "acme.example.com" 23 | ) 24 | 25 | func certificate() *unstructured.Unstructured { 26 | obj := &unstructured.Unstructured{} 27 | obj.SetGroupVersionKind(certManagerGroupVersion.WithKind(CertificateKind)) 28 | return obj 29 | } 30 | 31 | func certificateList() *unstructured.UnstructuredList { 32 | obj := &unstructured.UnstructuredList{} 33 | obj.SetGroupVersionKind(certManagerGroupVersion.WithKind(CertificateListKind)) 34 | return obj 35 | } 36 | 37 | func dnsEndpoint() *unstructured.Unstructured { 38 | obj := &unstructured.Unstructured{} 39 | obj.SetGroupVersionKind(externalDNSGroupVersion.WithKind(DNSEndpointKind)) 40 | return obj 41 | } 42 | 43 | func dnsEndpointList() *unstructured.UnstructuredList { 44 | obj := &unstructured.UnstructuredList{} 45 | obj.SetGroupVersionKind(externalDNSGroupVersion.WithKind(DNSEndpointListKind)) 46 | return obj 47 | } 48 | 49 | func tlsCertificateDelegation() *unstructured.Unstructured { 50 | obj := &unstructured.Unstructured{} 51 | obj.SetGroupVersionKind(contourGroupVersion.WithKind(TLSCertificateDelegationKind)) 52 | return obj 53 | } 54 | 55 | func tlsCertificateDelegationList() *unstructured.UnstructuredList { 56 | obj := &unstructured.UnstructuredList{} 57 | obj.SetGroupVersionKind(contourGroupVersion.WithKind(TLSCertificateDelegationListKind)) 58 | return obj 59 | } 60 | 61 | func testHTTPProxyReconcile() { 62 | It("should create DNSEndpoint and Certificate", func() { 63 | ns := testNamespacePrefix + randomString(10) 64 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 65 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 66 | })).ShouldNot(HaveOccurred()) 67 | 68 | scm, mgr := setupManager() 69 | 70 | prefix := "test-" 71 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 72 | ServiceKey: testServiceKey, 73 | Prefix: prefix, 74 | DefaultIssuerName: "test-issuer", 75 | DefaultIssuerKind: IssuerKind, 76 | CreateDNSEndpoint: true, 77 | CreateCertificate: true, 78 | })).ShouldNot(HaveOccurred()) 79 | 80 | stopMgr := startTestManager(mgr) 81 | defer stopMgr() 82 | 83 | By("creating HTTPProxy") 84 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 85 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 86 | 87 | By("getting DNSEndpoint with prefixed name") 88 | de := dnsEndpoint() 89 | objKey := client.ObjectKey{ 90 | Name: prefix + hpKey.Name, 91 | Namespace: hpKey.Namespace, 92 | } 93 | Eventually(func() error { 94 | return k8sClient.Get(context.Background(), objKey, de) 95 | }, 5*time.Second).Should(Succeed()) 96 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 97 | endPoints := deSpec["endpoints"].([]interface{}) 98 | endPoint := endPoints[0].(map[string]interface{}) 99 | Expect(endPoint["targets"]).Should(Equal([]interface{}{"10.0.0.0"})) 100 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 101 | 102 | By("ensuring additional DNSEndpoint does not exist") 103 | dde := dnsEndpoint() 104 | dObjKey := client.ObjectKey{ 105 | Name: prefix + hpKey.Name + "-delegation", 106 | Namespace: hpKey.Namespace, 107 | } 108 | Consistently(func() error { 109 | return k8sClient.Get(context.Background(), dObjKey, dde) 110 | }, 5*time.Second).ShouldNot(Succeed()) 111 | 112 | By("getting Certificate with prefixed name") 113 | crt := certificate() 114 | Eventually(func() error { 115 | return k8sClient.Get(context.Background(), objKey, crt) 116 | }).Should(Succeed()) 117 | 118 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 119 | Expect(crtSpec["dnsNames"]).Should(Equal([]interface{}{dnsName})) 120 | Expect(crtSpec["secretName"]).Should(Equal(testSecretName)) 121 | Expect(crtSpec["commonName"]).Should(Equal(dnsName)) 122 | Expect(crtSpec["usages"]).Should(Equal([]interface{}{ 123 | usageDigitalSignature, 124 | usageKeyEncipherment, 125 | usageServerAuth, 126 | })) 127 | Expect(crtSpec["revisionHistoryLimit"]).Should(BeNil()) 128 | }) 129 | 130 | It(`should not create DNSEndpoint and Certificate if "contour-plus.cybozu.com/exclude"" is "true"`, func() { 131 | ns := testNamespacePrefix + randomString(10) 132 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 133 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 134 | })).ShouldNot(HaveOccurred()) 135 | 136 | scm, mgr := setupManager() 137 | 138 | prefix := "test-" 139 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 140 | ServiceKey: testServiceKey, 141 | Prefix: prefix, 142 | DefaultIssuerName: "test-issuer", 143 | DefaultIssuerKind: IssuerKind, 144 | CreateDNSEndpoint: true, 145 | CreateCertificate: true, 146 | })).ShouldNot(HaveOccurred()) 147 | 148 | stopMgr := startTestManager(mgr) 149 | defer stopMgr() 150 | 151 | By("creating HTTPProxy having the annotation to exclude from contour-plus's targets") 152 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 153 | hp := newDummyHTTPProxy(hpKey) 154 | hp.Annotations[excludeAnnotation] = "true" 155 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 156 | 157 | By("confirming that DNSEndpoint and Certificate do not exist") 158 | time.Sleep(time.Second) 159 | endpointList := dnsEndpointList() 160 | Expect(k8sClient.List(context.Background(), endpointList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 161 | Expect(endpointList.Items).Should(BeEmpty()) 162 | 163 | crtList := certificateList() 164 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 165 | Expect(crtList.Items).Should(BeEmpty()) 166 | }) 167 | 168 | It("should create delegation DNSEndpoint if requested", func() { 169 | ns := testNamespacePrefix + randomString(10) 170 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 171 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 172 | })).ShouldNot(HaveOccurred()) 173 | 174 | scm, mgr := setupManager() 175 | 176 | prefix := "test-" 177 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 178 | ServiceKey: testServiceKey, 179 | Prefix: prefix, 180 | DefaultIssuerName: "test-issuer", 181 | DefaultIssuerKind: IssuerKind, 182 | DefaultDelegatedDomain: testDelegationName, 183 | CreateDNSEndpoint: true, 184 | CreateCertificate: true, 185 | })).ShouldNot(HaveOccurred()) 186 | 187 | stopMgr := startTestManager(mgr) 188 | defer stopMgr() 189 | 190 | By("creating HTTPProxy") 191 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 192 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 193 | 194 | By("getting DNSEndpoint with prefixed name") 195 | de := dnsEndpoint() 196 | objKey := client.ObjectKey{ 197 | Name: prefix + hpKey.Name, 198 | Namespace: hpKey.Namespace, 199 | } 200 | Eventually(func() error { 201 | return k8sClient.Get(context.Background(), objKey, de) 202 | }, 5*time.Second).Should(Succeed()) 203 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 204 | endPoints := deSpec["endpoints"].([]interface{}) 205 | endPoint := endPoints[0].(map[string]interface{}) 206 | Expect(endPoint["targets"]).Should(Equal([]interface{}{"10.0.0.0"})) 207 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 208 | 209 | By("ensuring additional DNSEndpoint has been created") 210 | dde := dnsEndpoint() 211 | dObjKey := client.ObjectKey{ 212 | Name: prefix + hpKey.Name + "-delegation", 213 | Namespace: hpKey.Namespace, 214 | } 215 | Eventually(func() error { 216 | return k8sClient.Get(context.Background(), dObjKey, dde) 217 | }, 5*time.Second).Should(Succeed()) 218 | ddeSpec := dde.UnstructuredContent()["spec"].(map[string]interface{}) 219 | dEndPoints := ddeSpec["endpoints"].([]interface{}) 220 | dEndPoint := dEndPoints[0].(map[string]interface{}) 221 | Expect(dEndPoint["targets"]).Should(Equal([]interface{}{"_acme-challenge." + dnsName + "." + testDelegationName})) 222 | Expect(dEndPoint["dnsName"]).Should(Equal("_acme-challenge." + dnsName)) 223 | Expect(dEndPoint["recordType"]).Should(Equal("CNAME")) 224 | }) 225 | 226 | It("should create delegation DNSEndpoint if requested via annotation", func() { 227 | ns := testNamespacePrefix + randomString(10) 228 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 229 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 230 | })).ShouldNot(HaveOccurred()) 231 | 232 | scm, mgr := setupManager() 233 | 234 | prefix := "test-" 235 | customDelegationName := "test." + testDelegationName 236 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 237 | ServiceKey: testServiceKey, 238 | Prefix: prefix, 239 | DefaultIssuerName: "test-issuer", 240 | DefaultIssuerKind: IssuerKind, 241 | DefaultDelegatedDomain: testDelegationName, 242 | AllowedDelegatedDomains: []string{customDelegationName}, 243 | AllowCustomDelegations: true, 244 | CreateDNSEndpoint: true, 245 | CreateCertificate: true, 246 | })).ShouldNot(HaveOccurred()) 247 | 248 | stopMgr := startTestManager(mgr) 249 | defer stopMgr() 250 | 251 | By("creating HTTPProxy") 252 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 253 | hp := newDummyHTTPProxy(hpKey) 254 | hp.Annotations[delegatedDomainAnnotation] = customDelegationName 255 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 256 | 257 | By("getting DNSEndpoint with prefixed name") 258 | de := dnsEndpoint() 259 | objKey := client.ObjectKey{ 260 | Name: prefix + hpKey.Name, 261 | Namespace: hpKey.Namespace, 262 | } 263 | Eventually(func() error { 264 | return k8sClient.Get(context.Background(), objKey, de) 265 | }, 5*time.Second).Should(Succeed()) 266 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 267 | endPoints := deSpec["endpoints"].([]interface{}) 268 | endPoint := endPoints[0].(map[string]interface{}) 269 | Expect(endPoint["targets"]).Should(Equal([]interface{}{"10.0.0.0"})) 270 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 271 | 272 | By("ensuring additional DNSEndpoint has been created") 273 | dde := dnsEndpoint() 274 | dObjKey := client.ObjectKey{ 275 | Name: prefix + hpKey.Name + "-delegation", 276 | Namespace: hpKey.Namespace, 277 | } 278 | Eventually(func() error { 279 | return k8sClient.Get(context.Background(), dObjKey, dde) 280 | }, 5*time.Second).Should(Succeed()) 281 | ddeSpec := dde.UnstructuredContent()["spec"].(map[string]interface{}) 282 | dEndPoints := ddeSpec["endpoints"].([]interface{}) 283 | dEndPoint := dEndPoints[0].(map[string]interface{}) 284 | Expect(dEndPoint["targets"]).Should(Equal([]interface{}{"_acme-challenge." + dnsName + "." + customDelegationName})) 285 | Expect(dEndPoint["dnsName"]).Should(Equal("_acme-challenge." + dnsName)) 286 | Expect(dEndPoint["recordType"]).Should(Equal("CNAME")) 287 | }) 288 | 289 | It("should ignore custom delegated domain if not permitted", func() { 290 | ns := testNamespacePrefix + randomString(10) 291 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 292 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 293 | })).ShouldNot(HaveOccurred()) 294 | 295 | scm, mgr := setupManager() 296 | 297 | prefix := "test-" 298 | customDelegationName := "test." + testDelegationName 299 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 300 | ServiceKey: testServiceKey, 301 | Prefix: prefix, 302 | DefaultIssuerName: "test-issuer", 303 | DefaultIssuerKind: IssuerKind, 304 | DefaultDelegatedDomain: testDelegationName, 305 | AllowCustomDelegations: true, 306 | CreateDNSEndpoint: true, 307 | CreateCertificate: true, 308 | })).ShouldNot(HaveOccurred()) 309 | 310 | stopMgr := startTestManager(mgr) 311 | defer stopMgr() 312 | 313 | By("creating HTTPProxy") 314 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 315 | hp := newDummyHTTPProxy(hpKey) 316 | hp.Annotations[delegatedDomainAnnotation] = customDelegationName 317 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 318 | 319 | By("getting DNSEndpoint with prefixed name") 320 | de := dnsEndpoint() 321 | objKey := client.ObjectKey{ 322 | Name: prefix + hpKey.Name, 323 | Namespace: hpKey.Namespace, 324 | } 325 | Eventually(func() error { 326 | return k8sClient.Get(context.Background(), objKey, de) 327 | }, 5*time.Second).Should(Succeed()) 328 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 329 | endPoints := deSpec["endpoints"].([]interface{}) 330 | endPoint := endPoints[0].(map[string]interface{}) 331 | Expect(endPoint["targets"]).Should(Equal([]interface{}{"10.0.0.0"})) 332 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 333 | 334 | By("ensuring additional DNSEndpoint has been created") 335 | dde := dnsEndpoint() 336 | dObjKey := client.ObjectKey{ 337 | Name: prefix + hpKey.Name + "-delegation", 338 | Namespace: hpKey.Namespace, 339 | } 340 | Eventually(func() error { 341 | return k8sClient.Get(context.Background(), dObjKey, dde) 342 | }, 5*time.Second).Should(Succeed()) 343 | ddeSpec := dde.UnstructuredContent()["spec"].(map[string]interface{}) 344 | dEndPoints := ddeSpec["endpoints"].([]interface{}) 345 | dEndPoint := dEndPoints[0].(map[string]interface{}) 346 | Expect(dEndPoint["targets"]).Should(Equal([]interface{}{"_acme-challenge." + dnsName + "." + testDelegationName})) 347 | Expect(dEndPoint["dnsName"]).Should(Equal("_acme-challenge." + dnsName)) 348 | Expect(dEndPoint["recordType"]).Should(Equal("CNAME")) 349 | }) 350 | 351 | It("should ignore custom delegated domain if not whitelisted", func() { 352 | ns := testNamespacePrefix + randomString(10) 353 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 354 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 355 | })).ShouldNot(HaveOccurred()) 356 | 357 | scm, mgr := setupManager() 358 | 359 | prefix := "test-" 360 | customDelegationName := "test." + testDelegationName 361 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 362 | ServiceKey: testServiceKey, 363 | Prefix: prefix, 364 | DefaultIssuerName: "test-issuer", 365 | DefaultIssuerKind: IssuerKind, 366 | DefaultDelegatedDomain: testDelegationName, 367 | CreateDNSEndpoint: true, 368 | CreateCertificate: true, 369 | })).ShouldNot(HaveOccurred()) 370 | 371 | stopMgr := startTestManager(mgr) 372 | defer stopMgr() 373 | 374 | By("creating HTTPProxy") 375 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 376 | hp := newDummyHTTPProxy(hpKey) 377 | hp.Annotations[delegatedDomainAnnotation] = customDelegationName 378 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 379 | 380 | By("getting DNSEndpoint with prefixed name") 381 | de := dnsEndpoint() 382 | objKey := client.ObjectKey{ 383 | Name: prefix + hpKey.Name, 384 | Namespace: hpKey.Namespace, 385 | } 386 | Eventually(func() error { 387 | return k8sClient.Get(context.Background(), objKey, de) 388 | }, 5*time.Second).Should(Succeed()) 389 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 390 | endPoints := deSpec["endpoints"].([]interface{}) 391 | endPoint := endPoints[0].(map[string]interface{}) 392 | Expect(endPoint["targets"]).Should(Equal([]interface{}{"10.0.0.0"})) 393 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 394 | 395 | By("ensuring additional DNSEndpoint has been created") 396 | dde := dnsEndpoint() 397 | dObjKey := client.ObjectKey{ 398 | Name: prefix + hpKey.Name + "-delegation", 399 | Namespace: hpKey.Namespace, 400 | } 401 | Eventually(func() error { 402 | return k8sClient.Get(context.Background(), dObjKey, dde) 403 | }, 5*time.Second).Should(Succeed()) 404 | ddeSpec := dde.UnstructuredContent()["spec"].(map[string]interface{}) 405 | dEndPoints := ddeSpec["endpoints"].([]interface{}) 406 | dEndPoint := dEndPoints[0].(map[string]interface{}) 407 | Expect(dEndPoint["targets"]).Should(Equal([]interface{}{"_acme-challenge." + dnsName + "." + testDelegationName})) 408 | Expect(dEndPoint["dnsName"]).Should(Equal("_acme-challenge." + dnsName)) 409 | Expect(dEndPoint["recordType"]).Should(Equal("CNAME")) 410 | }) 411 | 412 | It("should create Certificate with specified IssuerKind", func() { 413 | ns := testNamespacePrefix + randomString(10) 414 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 415 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 416 | })).ShouldNot(HaveOccurred()) 417 | 418 | By("setup manager with ClusterIssuer") 419 | scm, mgr := setupManager() 420 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 421 | ServiceKey: testServiceKey, 422 | DefaultIssuerName: "test-issuer", 423 | DefaultIssuerKind: ClusterIssuerKind, 424 | CreateCertificate: true, 425 | })).ShouldNot(HaveOccurred()) 426 | 427 | stopMgr := startTestManager(mgr) 428 | defer stopMgr() 429 | 430 | By("creating HTTPProxy") 431 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 432 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 433 | 434 | By("getting Certificate") 435 | crt := certificate() 436 | objKey := client.ObjectKey{Name: hpKey.Name, Namespace: hpKey.Namespace} 437 | Eventually(func() error { 438 | return k8sClient.Get(context.Background(), objKey, crt) 439 | }, 5*time.Second).Should(Succeed()) 440 | 441 | By("confirming that specified issuer used") 442 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 443 | issuerRef := crtSpec["issuerRef"].(map[string]interface{}) 444 | Expect(issuerRef["kind"]).Should(Equal(ClusterIssuerKind)) 445 | Expect(issuerRef["name"]).Should(Equal("test-issuer")) 446 | }) 447 | 448 | It(`should create Certificate with Issuer specified in "cert-manager.io/issuer"`, func() { 449 | ns := testNamespacePrefix + randomString(10) 450 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 451 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 452 | })).ShouldNot(HaveOccurred()) 453 | 454 | By("setup manager") 455 | scm, mgr := setupManager() 456 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 457 | ServiceKey: testServiceKey, 458 | DefaultIssuerName: "test-issuer", 459 | DefaultIssuerKind: IssuerKind, 460 | CreateCertificate: true, 461 | })).ShouldNot(HaveOccurred()) 462 | 463 | stopMgr := startTestManager(mgr) 464 | defer stopMgr() 465 | 466 | By("creating HTTPProxy with annotations") 467 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 468 | hp := newDummyHTTPProxy(hpKey) 469 | hp.Annotations[issuerNameAnnotation] = "custom-issuer" 470 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 471 | 472 | By("getting Certificate") 473 | crt := certificate() 474 | objKey := client.ObjectKey{Name: hpKey.Name, Namespace: hpKey.Namespace} 475 | Eventually(func() error { 476 | return k8sClient.Get(context.Background(), objKey, crt) 477 | }, 5*time.Second).Should(Succeed()) 478 | 479 | By("confirming that specified issuer used") 480 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 481 | issuerRef := crtSpec["issuerRef"].(map[string]interface{}) 482 | Expect(issuerRef["kind"]).Should(Equal(IssuerKind)) 483 | Expect(issuerRef["name"]).Should(Equal("custom-issuer")) 484 | 485 | }) 486 | 487 | It(`should create Certificate with Issuer specified in "cert-manager.io/cluster-issuer"`, func() { 488 | ns := testNamespacePrefix + randomString(10) 489 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 490 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 491 | })).ShouldNot(HaveOccurred()) 492 | 493 | By("setup manager") 494 | scm, mgr := setupManager() 495 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 496 | ServiceKey: testServiceKey, 497 | DefaultIssuerName: "test-issuer", 498 | DefaultIssuerKind: IssuerKind, 499 | CreateCertificate: true, 500 | })).ShouldNot(HaveOccurred()) 501 | 502 | stopMgr := startTestManager(mgr) 503 | defer stopMgr() 504 | 505 | By("updating HTTPProxy with annotations, both of issuer and cluster-issuer are specified") 506 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 507 | hp := newDummyHTTPProxy(hpKey) 508 | hp.Annotations[issuerNameAnnotation] = "custom-issuer" 509 | hp.Annotations[clusterIssuerNameAnnotation] = "custom-cluster-issuer" 510 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 511 | 512 | By("getting Certificate") 513 | crt := certificate() 514 | objKey := client.ObjectKey{Name: hpKey.Name, Namespace: hpKey.Namespace} 515 | Eventually(func() error { 516 | return k8sClient.Get(context.Background(), objKey, crt) 517 | }, 5*time.Second).Should(Succeed()) 518 | 519 | By("confirming that specified issuer used, cluster-issuer is precedence over issuer") 520 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 521 | issuerRef := crtSpec["issuerRef"].(map[string]interface{}) 522 | Expect(issuerRef["kind"]).Should(Equal(ClusterIssuerKind)) 523 | Expect(issuerRef["name"]).Should(Equal("custom-cluster-issuer")) 524 | }) 525 | 526 | It("should create DNSEndpoint, but should not create Certificate, if `createCertificate` is false", func() { 527 | ns := testNamespacePrefix + randomString(10) 528 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 529 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 530 | })).ShouldNot(HaveOccurred()) 531 | 532 | By("disabling the feature to create Certificate") 533 | scm, mgr := setupManager() 534 | 535 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 536 | ServiceKey: testServiceKey, 537 | DefaultIssuerName: "test-issuer", 538 | DefaultIssuerKind: IssuerKind, 539 | CreateDNSEndpoint: true, 540 | CreateCertificate: false, 541 | })).ShouldNot(HaveOccurred()) 542 | 543 | stopMgr := startTestManager(mgr) 544 | defer stopMgr() 545 | 546 | By("creating HTTPProxy") 547 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 548 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 549 | 550 | By("getting DNSEndpoint") 551 | de := dnsEndpoint() 552 | objKey := client.ObjectKey{ 553 | Name: hpKey.Name, 554 | Namespace: hpKey.Namespace, 555 | } 556 | Eventually(func() error { 557 | return k8sClient.Get(context.Background(), objKey, de) 558 | }, 5*time.Second).Should(Succeed()) 559 | 560 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 561 | endPoints := deSpec["endpoints"].([]interface{}) 562 | endPoint := endPoints[0].(map[string]interface{}) 563 | Expect(endPoint["targets"]).Should(Equal([]interface{}{dummyLoadBalancerIP})) 564 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 565 | 566 | By("confirming that Certificate does not exist") 567 | time.Sleep(time.Second) 568 | crtList := certificateList() 569 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 570 | Expect(crtList.Items).Should(BeEmpty()) 571 | }) 572 | 573 | It("should create Certificate, but should not create DNSEndpoint, if `CreateDNSEndpoint` is false", func() { 574 | ns := testNamespacePrefix + randomString(10) 575 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 576 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 577 | })).ShouldNot(HaveOccurred()) 578 | 579 | By("disabling the feature to create DNSEndpoint") 580 | scm, mgr := setupManager() 581 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 582 | ServiceKey: testServiceKey, 583 | DefaultIssuerName: "test-issuer", 584 | DefaultIssuerKind: IssuerKind, 585 | CreateDNSEndpoint: false, 586 | CreateCertificate: true, 587 | })).ShouldNot(HaveOccurred()) 588 | 589 | stopMgr := startTestManager(mgr) 590 | defer stopMgr() 591 | 592 | By("creating HTTPProxy") 593 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 594 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 595 | 596 | By("getting Certificate") 597 | crt := certificate() 598 | objKey := client.ObjectKey{Name: hpKey.Name, Namespace: hpKey.Namespace} 599 | Eventually(func() error { 600 | return k8sClient.Get(context.Background(), objKey, crt) 601 | }, 5*time.Second).Should(Succeed()) 602 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 603 | Expect(crtSpec["secretName"]).Should(Equal(testSecretName)) 604 | 605 | By("confirming that DNSEndpoint does not exist") 606 | time.Sleep(time.Second) 607 | endpointList := dnsEndpointList() 608 | Expect(k8sClient.List(context.Background(), endpointList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 609 | Expect(endpointList.Items).Should(BeEmpty()) 610 | }) 611 | 612 | It(`should not create Certificate, if "kubernetes.io/tls-acme" is not "true"`, func() { 613 | ns := testNamespacePrefix + randomString(10) 614 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 615 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 616 | })).ShouldNot(HaveOccurred()) 617 | 618 | By("disabling the feature to create Certificate") 619 | scm, mgr := setupManager() 620 | 621 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 622 | ServiceKey: testServiceKey, 623 | DefaultIssuerName: "test-issuer", 624 | DefaultIssuerKind: IssuerKind, 625 | CreateDNSEndpoint: true, 626 | CreateCertificate: false, 627 | })).ShouldNot(HaveOccurred()) 628 | 629 | stopMgr := startTestManager(mgr) 630 | defer stopMgr() 631 | 632 | By("creating HTTPProxy") 633 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 634 | hp := newDummyHTTPProxy(hpKey) 635 | hp.Annotations[testACMETLSAnnotation] = "aaa" 636 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 637 | 638 | By("getting DNSEndpoint") 639 | de := dnsEndpoint() 640 | objKey := client.ObjectKey{ 641 | Name: hpKey.Name, 642 | Namespace: hpKey.Namespace, 643 | } 644 | Eventually(func() error { 645 | return k8sClient.Get(context.Background(), objKey, de) 646 | }, 5*time.Second).Should(Succeed()) 647 | deSpec := de.UnstructuredContent()["spec"].(map[string]interface{}) 648 | endPoints := deSpec["endpoints"].([]interface{}) 649 | endPoint := endPoints[0].(map[string]interface{}) 650 | Expect(endPoint["targets"]).Should(Equal([]interface{}{dummyLoadBalancerIP})) 651 | Expect(endPoint["dnsName"]).Should(Equal(dnsName)) 652 | 653 | By("confirming that Certificate does not exist") 654 | time.Sleep(time.Second) 655 | crtList := certificateList() 656 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 657 | Expect(crtList.Items).Should(BeEmpty()) 658 | }) 659 | 660 | It("should create Certificate, if `DefaultIssuerName` is empty but 'issuer-name' annotation is not empty", func() { 661 | ns := testNamespacePrefix + randomString(10) 662 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 663 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 664 | })).ShouldNot(HaveOccurred()) 665 | 666 | By("setup reconciler with empty DefaultIssuerName") 667 | scm, mgr := setupManager() 668 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 669 | ServiceKey: testServiceKey, 670 | DefaultIssuerKind: IssuerKind, 671 | CreateCertificate: true, 672 | })).ShouldNot(HaveOccurred()) 673 | 674 | stopMgr := startTestManager(mgr) 675 | defer stopMgr() 676 | 677 | By("creating HTTPProxy") 678 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 679 | hp := newDummyHTTPProxy(hpKey) 680 | hp.Annotations[issuerNameAnnotation] = "custom-issuer" 681 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 682 | 683 | By("getting Certificate with specified name") 684 | crt := certificate() 685 | Eventually(func() error { 686 | return k8sClient.Get(context.Background(), client.ObjectKey{Namespace: ns, Name: hpKey.Name}, crt) 687 | }).Should(Succeed()) 688 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 689 | issuerRef := crtSpec["issuerRef"].(map[string]interface{}) 690 | Expect(issuerRef["name"]).Should(Equal("custom-issuer")) 691 | Expect(issuerRef["kind"]).Should(Equal(IssuerKind)) 692 | }) 693 | 694 | It("should not create Certificate, if `DefaultIssuerName` and 'issuer-name' annotation are empty", func() { 695 | ns := testNamespacePrefix + randomString(10) 696 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 697 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 698 | })).ShouldNot(HaveOccurred()) 699 | 700 | By("setup reconciler with empty DefaultIssuerName") 701 | scm, mgr := setupManager() 702 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 703 | ServiceKey: testServiceKey, 704 | DefaultIssuerKind: IssuerKind, 705 | CreateCertificate: true, 706 | })).ShouldNot(HaveOccurred()) 707 | 708 | stopMgr := startTestManager(mgr) 709 | defer stopMgr() 710 | 711 | By("creating HTTPProxy") 712 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 713 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 714 | 715 | By("confirming that Certificate does not exist") 716 | time.Sleep(time.Second) 717 | crtList := certificateList() 718 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 719 | Expect(crtList.Items).Should(BeEmpty()) 720 | }) 721 | 722 | It(`should not create DNSEndpoint and Certificate if the class name is not the target`, func() { 723 | ns := testNamespacePrefix + randomString(10) 724 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 725 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 726 | })).ShouldNot(HaveOccurred()) 727 | 728 | scm, mgr := setupManager() 729 | 730 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 731 | ServiceKey: testServiceKey, 732 | DefaultIssuerName: "test-issuer", 733 | DefaultIssuerKind: IssuerKind, 734 | CreateDNSEndpoint: true, 735 | CreateCertificate: true, 736 | IngressClassName: "class-name", 737 | })).ShouldNot(HaveOccurred()) 738 | 739 | stopMgr := startTestManager(mgr) 740 | defer stopMgr() 741 | 742 | By("creating HTTPProxy having the annotation") 743 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 744 | hp := newDummyHTTPProxy(hpKey) 745 | hp.Annotations[ingressClassNameAnnotation] = "wrong" 746 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 747 | 748 | By("confirming that DNSEndpoint and Certificate do not exist") 749 | time.Sleep(time.Second) 750 | endpointList := dnsEndpointList() 751 | Expect(k8sClient.List(context.Background(), endpointList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 752 | Expect(endpointList.Items).Should(BeEmpty()) 753 | 754 | crtList := certificateList() 755 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 756 | Expect(crtList.Items).Should(BeEmpty()) 757 | 758 | By("creating HTTPProxy without the annotation") 759 | hpKey = client.ObjectKey{Name: "bar", Namespace: ns} 760 | hp = newDummyHTTPProxy(hpKey) 761 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 762 | 763 | By("confirming that DNSEndpoint and Certificate do not exist") 764 | time.Sleep(time.Second) 765 | endpointList = dnsEndpointList() 766 | Expect(k8sClient.List(context.Background(), endpointList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 767 | Expect(endpointList.Items).Should(BeEmpty()) 768 | 769 | crtList = certificateList() 770 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 771 | Expect(crtList.Items).Should(BeEmpty()) 772 | }) 773 | 774 | It(`should create Certificate if the class name equals to the target`, func() { 775 | ns := testNamespacePrefix + randomString(10) 776 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 777 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 778 | })).ShouldNot(HaveOccurred()) 779 | 780 | scm, mgr := setupManager() 781 | 782 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 783 | ServiceKey: testServiceKey, 784 | DefaultIssuerName: "test-issuer", 785 | DefaultIssuerKind: IssuerKind, 786 | CreateCertificate: true, 787 | IngressClassName: "class-name", 788 | })).ShouldNot(HaveOccurred()) 789 | 790 | stopMgr := startTestManager(mgr) 791 | defer stopMgr() 792 | 793 | By("creating HTTPProxy having the annotation") 794 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 795 | hp := newDummyHTTPProxy(hpKey) 796 | hp.Annotations[ingressClassNameAnnotation] = "class-name" 797 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 798 | 799 | By("getting Certificate") 800 | crt := certificate() 801 | objKey := client.ObjectKey{Name: hpKey.Name, Namespace: hpKey.Namespace} 802 | Eventually(func() error { 803 | return k8sClient.Get(context.Background(), objKey, crt) 804 | }, 5*time.Second).Should(Succeed()) 805 | }) 806 | 807 | It(`should create Certificate with revisionHistoryLimit set if specified`, func() { 808 | ns := testNamespacePrefix + randomString(10) 809 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 810 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 811 | })).ShouldNot(HaveOccurred()) 812 | 813 | scm, mgr := setupManager() 814 | 815 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 816 | ServiceKey: testServiceKey, 817 | DefaultIssuerName: "test-issuer", 818 | DefaultIssuerKind: IssuerKind, 819 | CreateCertificate: true, 820 | CSRRevisionLimit: 1, 821 | })).ShouldNot(HaveOccurred()) 822 | 823 | stopMgr := startTestManager(mgr) 824 | defer stopMgr() 825 | 826 | By("creating HTTPProxy") 827 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 828 | Expect(k8sClient.Create(context.Background(), newDummyHTTPProxy(hpKey))).ShouldNot(HaveOccurred()) 829 | 830 | By("getting Certificate") 831 | crt := certificate() 832 | objKey := client.ObjectKey{ 833 | Name: hpKey.Name, 834 | Namespace: hpKey.Namespace, 835 | } 836 | Eventually(func() error { 837 | return k8sClient.Get(context.Background(), objKey, crt) 838 | }).Should(Succeed()) 839 | 840 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 841 | Expect(crtSpec["dnsNames"]).Should(Equal([]interface{}{dnsName})) 842 | Expect(crtSpec["secretName"]).Should(Equal(testSecretName)) 843 | Expect(crtSpec["commonName"]).Should(Equal(dnsName)) 844 | Expect(crtSpec["usages"]).Should(Equal([]interface{}{ 845 | usageDigitalSignature, 846 | usageKeyEncipherment, 847 | usageServerAuth, 848 | })) 849 | Expect(crtSpec["revisionHistoryLimit"]).Should(Equal(int64(1))) 850 | }) 851 | 852 | It(`should create Certificate with revisionHistoryLimit set if cert-manager.io/revision-history-limit is specified`, func() { 853 | ns := testNamespacePrefix + randomString(10) 854 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 855 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 856 | })).ShouldNot(HaveOccurred()) 857 | 858 | scm, mgr := setupManager() 859 | 860 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 861 | ServiceKey: testServiceKey, 862 | DefaultIssuerName: "test-issuer", 863 | DefaultIssuerKind: IssuerKind, 864 | CreateCertificate: true, 865 | })).ShouldNot(HaveOccurred()) 866 | 867 | stopMgr := startTestManager(mgr) 868 | defer stopMgr() 869 | 870 | By("creating HTTPProxy") 871 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 872 | hp := newDummyHTTPProxy(hpKey) 873 | hp.Annotations[revisionHistoryLimitAnnotation] = "2" 874 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 875 | 876 | By("getting Certificate") 877 | crt := certificate() 878 | objKey := client.ObjectKey{ 879 | Name: hpKey.Name, 880 | Namespace: hpKey.Namespace, 881 | } 882 | Eventually(func() error { 883 | return k8sClient.Get(context.Background(), objKey, crt) 884 | }).Should(Succeed()) 885 | 886 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 887 | Expect(crtSpec["dnsNames"]).Should(Equal([]interface{}{dnsName})) 888 | Expect(crtSpec["secretName"]).Should(Equal(testSecretName)) 889 | Expect(crtSpec["commonName"]).Should(Equal(dnsName)) 890 | Expect(crtSpec["usages"]).Should(Equal([]interface{}{ 891 | usageDigitalSignature, 892 | usageKeyEncipherment, 893 | usageServerAuth, 894 | })) 895 | Expect(crtSpec["revisionHistoryLimit"]).Should(Equal(int64(2))) 896 | }) 897 | 898 | It(`should prioritize revisionHistoryLimit set by cert-manager.io/revision-history-limit is specified`, func() { 899 | ns := testNamespacePrefix + randomString(10) 900 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 901 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 902 | })).ShouldNot(HaveOccurred()) 903 | 904 | scm, mgr := setupManager() 905 | 906 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 907 | ServiceKey: testServiceKey, 908 | DefaultIssuerName: "test-issuer", 909 | DefaultIssuerKind: IssuerKind, 910 | CreateCertificate: true, 911 | CSRRevisionLimit: 1, 912 | })).ShouldNot(HaveOccurred()) 913 | 914 | stopMgr := startTestManager(mgr) 915 | defer stopMgr() 916 | 917 | By("creating HTTPProxy") 918 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 919 | hp := newDummyHTTPProxy(hpKey) 920 | hp.Annotations[revisionHistoryLimitAnnotation] = "2" 921 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 922 | 923 | By("getting Certificate") 924 | crt := certificate() 925 | objKey := client.ObjectKey{ 926 | Name: hpKey.Name, 927 | Namespace: hpKey.Namespace, 928 | } 929 | Eventually(func() error { 930 | return k8sClient.Get(context.Background(), objKey, crt) 931 | }).Should(Succeed()) 932 | 933 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 934 | Expect(crtSpec["dnsNames"]).Should(Equal([]interface{}{dnsName})) 935 | Expect(crtSpec["secretName"]).Should(Equal(testSecretName)) 936 | Expect(crtSpec["commonName"]).Should(Equal(dnsName)) 937 | Expect(crtSpec["usages"]).Should(Equal([]interface{}{ 938 | usageDigitalSignature, 939 | usageKeyEncipherment, 940 | usageServerAuth, 941 | })) 942 | Expect(crtSpec["revisionHistoryLimit"]).Should(Equal(int64(2))) 943 | }) 944 | 945 | It("should create a Certificate with the specified key algorithm and default size", func() { 946 | ns := testNamespacePrefix + randomString(10) 947 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 948 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 949 | })).ShouldNot(HaveOccurred()) 950 | 951 | scm, mgr := setupManager() 952 | 953 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 954 | ServiceKey: testServiceKey, 955 | DefaultIssuerName: "test-issuer", 956 | DefaultIssuerKind: IssuerKind, 957 | CreateCertificate: true, 958 | })).ShouldNot(HaveOccurred()) 959 | 960 | stopMgr := startTestManager(mgr) 961 | defer stopMgr() 962 | 963 | By("creating HTTPProxy with key algorithm annotation") 964 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 965 | hp := newDummyHTTPProxy(hpKey) 966 | hp.Annotations[privateKeyAlgorithmAnnotation] = "ECDSA" 967 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 968 | 969 | By("getting Certificate") 970 | crt := certificate() 971 | objKey := client.ObjectKey{ 972 | Name: hpKey.Name, 973 | Namespace: hpKey.Namespace, 974 | } 975 | Eventually(func() error { 976 | return k8sClient.Get(context.Background(), objKey, crt) 977 | }).Should(Succeed()) 978 | 979 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 980 | keySpec := crtSpec["privateKey"].(map[string]interface{}) 981 | Expect(keySpec["algorithm"]).Should(Equal("ECDSA")) 982 | Expect(keySpec["size"]).Should(BeNil()) 983 | }) 984 | 985 | It("should create a Certificate with the specified key algorithm and size", func() { 986 | ns := testNamespacePrefix + randomString(10) 987 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 988 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 989 | })).ShouldNot(HaveOccurred()) 990 | 991 | scm, mgr := setupManager() 992 | 993 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 994 | ServiceKey: testServiceKey, 995 | DefaultIssuerName: "test-issuer", 996 | DefaultIssuerKind: IssuerKind, 997 | CreateCertificate: true, 998 | })).ShouldNot(HaveOccurred()) 999 | 1000 | stopMgr := startTestManager(mgr) 1001 | defer stopMgr() 1002 | 1003 | By("creating HTTPProxy with key algorithm annotation") 1004 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1005 | hp := newDummyHTTPProxy(hpKey) 1006 | hp.Annotations[privateKeyAlgorithmAnnotation] = "ECDSA" 1007 | hp.Annotations[privateKeySizeAnnotation] = "384" 1008 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1009 | 1010 | By("getting Certificate") 1011 | crt := certificate() 1012 | objKey := client.ObjectKey{ 1013 | Name: hpKey.Name, 1014 | Namespace: hpKey.Namespace, 1015 | } 1016 | Eventually(func() error { 1017 | return k8sClient.Get(context.Background(), objKey, crt) 1018 | }).Should(Succeed()) 1019 | 1020 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 1021 | keySpec := crtSpec["privateKey"].(map[string]interface{}) 1022 | Expect(keySpec["algorithm"]).Should(Equal("ECDSA")) 1023 | Expect(keySpec["size"]).Should(Equal(int64(384))) 1024 | }) 1025 | 1026 | It("should create a Certificate with the specified key algorithm and fallback to default size", func() { 1027 | ns := testNamespacePrefix + randomString(10) 1028 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1029 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1030 | })).ShouldNot(HaveOccurred()) 1031 | 1032 | scm, mgr := setupManager() 1033 | 1034 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1035 | ServiceKey: testServiceKey, 1036 | DefaultIssuerName: "test-issuer", 1037 | DefaultIssuerKind: IssuerKind, 1038 | CreateCertificate: true, 1039 | })).ShouldNot(HaveOccurred()) 1040 | 1041 | stopMgr := startTestManager(mgr) 1042 | defer stopMgr() 1043 | 1044 | By("creating HTTPProxy with key algorithm annotation") 1045 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1046 | hp := newDummyHTTPProxy(hpKey) 1047 | hp.Annotations[privateKeyAlgorithmAnnotation] = "RSA" 1048 | hp.Annotations[privateKeySizeAnnotation] = "four thousand ninty six" 1049 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1050 | 1051 | By("getting Certificate") 1052 | crt := certificate() 1053 | objKey := client.ObjectKey{ 1054 | Name: hpKey.Name, 1055 | Namespace: hpKey.Namespace, 1056 | } 1057 | Eventually(func() error { 1058 | return k8sClient.Get(context.Background(), objKey, crt) 1059 | }).Should(Succeed()) 1060 | 1061 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 1062 | keySpec := crtSpec["privateKey"].(map[string]interface{}) 1063 | Expect(keySpec["algorithm"]).Should(Equal("RSA")) 1064 | Expect(keySpec["size"]).Should(BeNil()) 1065 | }) 1066 | 1067 | It("should propagate annotations to the generated resources", func() { 1068 | ns := testNamespacePrefix + randomString(10) 1069 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1070 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1071 | })).ShouldNot(HaveOccurred()) 1072 | 1073 | scm, mgr := setupManager() 1074 | 1075 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1076 | ServiceKey: testServiceKey, 1077 | DefaultIssuerName: "test-issuer", 1078 | DefaultIssuerKind: IssuerKind, 1079 | CreateCertificate: true, 1080 | CreateDNSEndpoint: true, 1081 | PropagatedAnnotations: []string{ 1082 | "example.com/propagate-me", 1083 | }, 1084 | })).ShouldNot(HaveOccurred()) 1085 | 1086 | stopMgr := startTestManager(mgr) 1087 | defer stopMgr() 1088 | 1089 | By("creating HTTPProxy with annotations") 1090 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1091 | hp := newDummyHTTPProxy(hpKey) 1092 | hp.Annotations["example.com/propagate-me"] = "yes" 1093 | hp.Annotations["example.com/do-not-propagate-me"] = "yes" 1094 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1095 | 1096 | By("getting Certificate") 1097 | crt := certificate() 1098 | objKey := client.ObjectKey{ 1099 | Name: hpKey.Name, 1100 | Namespace: hpKey.Namespace, 1101 | } 1102 | Eventually(func() error { 1103 | return k8sClient.Get(context.Background(), objKey, crt) 1104 | }).Should(Succeed()) 1105 | 1106 | crtAnnotations := crt.GetAnnotations() 1107 | Expect(crtAnnotations).ToNot(BeNil()) 1108 | Expect(crtAnnotations["example.com/propagate-me"]).To(Equal("yes")) 1109 | Expect(crtAnnotations).ToNot(HaveKey("example.com/do-not-propagate-me")) 1110 | 1111 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 1112 | Expect(crtSpec).ToNot(BeNil()) 1113 | secretTemplate := crtSpec["secretTemplate"].(map[string]interface{}) 1114 | Expect(secretTemplate).ToNot(BeNil()) 1115 | secretAnnotations := secretTemplate["annotations"].(map[string]interface{}) 1116 | Expect(secretAnnotations).ToNot(BeNil()) 1117 | Expect(secretAnnotations["example.com/propagate-me"]).To(Equal("yes")) 1118 | Expect(secretAnnotations).ToNot(HaveKey("example.com/do-not-propagate-me")) 1119 | 1120 | By("getting DNSEndpoint") 1121 | de := dnsEndpoint() 1122 | Eventually(func() error { 1123 | return k8sClient.Get(context.Background(), objKey, de) 1124 | }).Should(Succeed()) 1125 | 1126 | deAnnotations := de.GetAnnotations() 1127 | Expect(deAnnotations).ToNot(BeNil()) 1128 | Expect(deAnnotations["example.com/propagate-me"]).To(Equal("yes")) 1129 | Expect(deAnnotations).ToNot(HaveKey("example.com/do-not-propagate-me")) 1130 | }) 1131 | 1132 | It("should progatate labels to the generated resources", func() { 1133 | ns := testNamespacePrefix + randomString(10) 1134 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1135 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1136 | })).ShouldNot(HaveOccurred()) 1137 | 1138 | scm, mgr := setupManager() 1139 | 1140 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1141 | ServiceKey: testServiceKey, 1142 | DefaultIssuerName: "test-issuer", 1143 | DefaultIssuerKind: IssuerKind, 1144 | CreateCertificate: true, 1145 | CreateDNSEndpoint: true, 1146 | PropagatedLabels: []string{ 1147 | "example.com/propagate-me", 1148 | }, 1149 | })).ShouldNot(HaveOccurred()) 1150 | 1151 | stopMgr := startTestManager(mgr) 1152 | defer stopMgr() 1153 | 1154 | By("creating HTTPProxy with labels") 1155 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1156 | hp := newDummyHTTPProxy(hpKey) 1157 | hp.Labels = map[string]string{ 1158 | "example.com/propagate-me": "yes", 1159 | "example.com/do-not-propagate": "yes", 1160 | } 1161 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1162 | 1163 | By("getting Certificate") 1164 | crt := certificate() 1165 | objKey := client.ObjectKey{ 1166 | Name: hpKey.Name, 1167 | Namespace: hpKey.Namespace, 1168 | } 1169 | Eventually(func() error { 1170 | return k8sClient.Get(context.Background(), objKey, crt) 1171 | }).Should(Succeed()) 1172 | 1173 | crtLabels := crt.GetLabels() 1174 | Expect(crtLabels).ToNot(BeNil()) 1175 | Expect(crtLabels["example.com/propagate-me"]).To(Equal("yes")) 1176 | Expect(crtLabels).ToNot(HaveKey("example.com/do-not-propagate")) 1177 | 1178 | crtSpec := crt.UnstructuredContent()["spec"].(map[string]interface{}) 1179 | Expect(crtSpec).ToNot(BeNil()) 1180 | secretTemplate := crtSpec["secretTemplate"].(map[string]interface{}) 1181 | Expect(secretTemplate).ToNot(BeNil()) 1182 | secretLabels := secretTemplate["labels"].(map[string]interface{}) 1183 | Expect(secretLabels).ToNot(BeNil()) 1184 | Expect(secretLabels["example.com/propagate-me"]).To(Equal("yes")) 1185 | Expect(secretLabels).ToNot(HaveKey("example.com/do-not-propagate")) 1186 | 1187 | By("getting DNSEndpoint") 1188 | de := dnsEndpoint() 1189 | Eventually(func() error { 1190 | return k8sClient.Get(context.Background(), objKey, de) 1191 | }).Should(Succeed()) 1192 | 1193 | deLabels := de.GetLabels() 1194 | Expect(deLabels).ToNot(BeNil()) 1195 | Expect(deLabels["example.com/propagate-me"]).To(Equal("yes")) 1196 | Expect(deLabels).ToNot(HaveKey("example.com/do-not-propagate")) 1197 | }) 1198 | 1199 | It("should create a DNSEndpoint in the specified namespace", func() { 1200 | ns := testNamespacePrefix + randomString(10) 1201 | deNs := testNamespacePrefix + randomString(10) 1202 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1203 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1204 | })).ShouldNot(HaveOccurred()) 1205 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1206 | ObjectMeta: ctrl.ObjectMeta{Name: deNs}, 1207 | })).ShouldNot(HaveOccurred()) 1208 | 1209 | scm, mgr := setupManager() 1210 | 1211 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1212 | ServiceKey: testServiceKey, 1213 | CreateDNSEndpoint: true, 1214 | AllowedDNSNamespaces: []string{deNs}, 1215 | })).ShouldNot(HaveOccurred()) 1216 | 1217 | stopMgr := startTestManager(mgr) 1218 | defer stopMgr() 1219 | 1220 | By("creating HTTPProxy with DNSEndpoint namespace annotation") 1221 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1222 | hp := newDummyHTTPProxy(hpKey) 1223 | hp.Annotations[dnsNamespaceAnnotation] = deNs 1224 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1225 | 1226 | By("getting DNSEndpoint in the specified namespace") 1227 | de := dnsEndpoint() 1228 | objKey := client.ObjectKey{ 1229 | Name: hpKey.Namespace + "-" + hpKey.Name, 1230 | Namespace: deNs, 1231 | } 1232 | Eventually(func() error { 1233 | return k8sClient.Get(context.Background(), objKey, de) 1234 | }, 5*time.Second).Should(Succeed()) 1235 | 1236 | By("ensuring DNSEndpoint is not created in the HTTPProxy namespace") 1237 | deList := dnsEndpointList() 1238 | Expect(k8sClient.List(context.Background(), deList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 1239 | Expect(deList.Items).Should(BeEmpty()) 1240 | 1241 | By("ensuring HTTPProxy deletion deletes the DNSEndpoint") 1242 | Expect(k8sClient.Delete(context.Background(), hp)).ShouldNot(HaveOccurred()) 1243 | Eventually(func() error { 1244 | return k8sClient.Get(context.Background(), objKey, de) 1245 | }, 5*time.Second).ShouldNot(Succeed()) 1246 | }) 1247 | 1248 | It("should create DNSEndpoint and delegation DNSEndpoint in the specified namespace", func() { 1249 | ns := testNamespacePrefix + randomString(10) 1250 | deNs := testNamespacePrefix + randomString(10) 1251 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1252 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1253 | })).ShouldNot(HaveOccurred()) 1254 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1255 | ObjectMeta: ctrl.ObjectMeta{Name: deNs}, 1256 | })).ShouldNot(HaveOccurred()) 1257 | 1258 | scm, mgr := setupManager() 1259 | 1260 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1261 | ServiceKey: testServiceKey, 1262 | CreateDNSEndpoint: true, 1263 | DefaultDelegatedDomain: testDelegationName, 1264 | AllowedDNSNamespaces: []string{deNs}, 1265 | })).ShouldNot(HaveOccurred()) 1266 | 1267 | stopMgr := startTestManager(mgr) 1268 | defer stopMgr() 1269 | 1270 | By("creating HTTPProxy with DNSEndpoint namespace annotation") 1271 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1272 | hp := newDummyHTTPProxy(hpKey) 1273 | hp.Annotations[dnsNamespaceAnnotation] = deNs 1274 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1275 | 1276 | By("getting DNSEndpoint in the specified namespace") 1277 | de := dnsEndpoint() 1278 | objKey := client.ObjectKey{ 1279 | Name: hpKey.Namespace + "-" + hpKey.Name, 1280 | Namespace: deNs, 1281 | } 1282 | Eventually(func() error { 1283 | return k8sClient.Get(context.Background(), objKey, de) 1284 | }, 5*time.Second).Should(Succeed()) 1285 | 1286 | By("ensuring delegation DNSEndpoint is created in the specified namespace") 1287 | de = dnsEndpoint() 1288 | delObjKey := client.ObjectKey{ 1289 | Name: hpKey.Namespace + "-" + hpKey.Name + "-delegation", 1290 | Namespace: deNs, 1291 | } 1292 | Eventually(func() error { 1293 | return k8sClient.Get(context.Background(), delObjKey, de) 1294 | }, 5*time.Second).Should(Succeed()) 1295 | 1296 | By("ensuring DNSEndpoint is not created in the HTTPProxy namespace") 1297 | deList := dnsEndpointList() 1298 | Expect(k8sClient.List(context.Background(), deList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 1299 | Expect(deList.Items).Should(BeEmpty()) 1300 | 1301 | By("ensuring HTTPProxy deletion deletes the DNSEndpoint and delegation DNSEndpoint") 1302 | Expect(k8sClient.Delete(context.Background(), hp)).ShouldNot(HaveOccurred()) 1303 | Eventually(func() error { 1304 | return k8sClient.Get(context.Background(), objKey, de) 1305 | }, 5*time.Second).ShouldNot(Succeed()) 1306 | Eventually(func() error { 1307 | return k8sClient.Get(context.Background(), delObjKey, de) 1308 | }, 5*time.Second).ShouldNot(Succeed()) 1309 | }) 1310 | 1311 | It("should create Certificate and TLSCertificateDelegation in the specified namespace", func() { 1312 | ns := testNamespacePrefix + randomString(10) 1313 | certNs := testNamespacePrefix + randomString(10) 1314 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1315 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1316 | })).ShouldNot(HaveOccurred()) 1317 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1318 | ObjectMeta: ctrl.ObjectMeta{Name: certNs}, 1319 | })).ShouldNot(HaveOccurred()) 1320 | 1321 | scm, mgr := setupManager() 1322 | 1323 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1324 | ServiceKey: testServiceKey, 1325 | CreateCertificate: true, 1326 | DefaultIssuerKind: IssuerKind, 1327 | DefaultIssuerName: "test-issuer", 1328 | AllowedIssuerNamespaces: []string{certNs}, 1329 | })).ShouldNot(HaveOccurred()) 1330 | 1331 | stopMgr := startTestManager(mgr) 1332 | defer stopMgr() 1333 | 1334 | By("creating HTTPProxy with Certificate namespace annotation") 1335 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1336 | hp := newDummyHTTPProxy(hpKey) 1337 | hp.Spec.VirtualHost.TLS = nil 1338 | hp.Annotations[issuerNamespaceAnnotation] = certNs 1339 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1340 | 1341 | certName := hpKey.Namespace + "-" + hpKey.Name 1342 | By("getting Certificate in the specified namespace") 1343 | crt := certificate() 1344 | objKey := client.ObjectKey{ 1345 | Name: certName, 1346 | Namespace: certNs, 1347 | } 1348 | Eventually(func() error { 1349 | return k8sClient.Get(context.Background(), objKey, crt) 1350 | }, 5*time.Second).Should(Succeed()) 1351 | 1352 | By("ensuring TLSCertificateDelegation is created in the specified namespace") 1353 | tcd := tlsCertificateDelegation() 1354 | Eventually(func() error { 1355 | return k8sClient.Get(context.Background(), objKey, tcd) 1356 | }, 5*time.Second).Should(Succeed()) 1357 | tcdSpec := tcd.UnstructuredContent()["spec"].(map[string]interface{}) 1358 | delegations := tcdSpec["delegations"].([]interface{}) 1359 | delegation := delegations[0].(map[string]interface{}) 1360 | Expect(len(delegations)).Should(Equal(1)) 1361 | secretName := delegation["secretName"].(string) 1362 | Expect(secretName).Should(Equal(certName)) 1363 | targetNamespaces := delegation["targetNamespaces"].([]interface{}) 1364 | Expect(len(targetNamespaces)).Should(Equal(1)) 1365 | Expect(targetNamespaces[0].(string)).Should(Equal(hpKey.Namespace)) 1366 | 1367 | By("ensuring Certificate is not created in the HTTPProxy namespace") 1368 | crtList := certificateList() 1369 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(ns))).ShouldNot(HaveOccurred()) 1370 | Expect(crtList.Items).Should(BeEmpty()) 1371 | 1372 | By("ensuring HTTPProxy references the namespaced Certificate") 1373 | hpObj := &projectcontourv1.HTTPProxy{} 1374 | Expect(k8sClient.Get(context.Background(), hpKey, hpObj)).ShouldNot(HaveOccurred()) 1375 | Expect(hpObj.Spec.VirtualHost.TLS.SecretName).Should(Equal(certNs + "/" + certName)) 1376 | 1377 | By("ensuring HTTPProxy deletion deletes the Certificate and TLSCertificateDelegation") 1378 | Expect(k8sClient.Delete(context.Background(), hp)).ShouldNot(HaveOccurred()) 1379 | Eventually(func() error { 1380 | return k8sClient.Get(context.Background(), objKey, crt) 1381 | }, 5*time.Second).ShouldNot(Succeed()) 1382 | Eventually(func() error { 1383 | return k8sClient.Get(context.Background(), objKey, tcd) 1384 | }, 5*time.Second).ShouldNot(Succeed()) 1385 | }) 1386 | 1387 | It("should not create DNSEndpoint if the namespace is not allowed", func() { 1388 | ns := testNamespacePrefix + randomString(10) 1389 | deNs := testNamespacePrefix + randomString(10) 1390 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1391 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1392 | })).ShouldNot(HaveOccurred()) 1393 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1394 | ObjectMeta: ctrl.ObjectMeta{Name: deNs}, 1395 | })).ShouldNot(HaveOccurred()) 1396 | 1397 | scm, mgr := setupManager() 1398 | 1399 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1400 | ServiceKey: testServiceKey, 1401 | CreateDNSEndpoint: true, 1402 | AllowedDNSNamespaces: []string{}, 1403 | })).ShouldNot(HaveOccurred()) 1404 | 1405 | stopMgr := startTestManager(mgr) 1406 | defer stopMgr() 1407 | 1408 | By("creating HTTPProxy with DNSEndpoint namespace annotation") 1409 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1410 | hp := newDummyHTTPProxy(hpKey) 1411 | hp.Annotations[dnsNamespaceAnnotation] = deNs 1412 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1413 | 1414 | By("confirming that DNSEndpoint does not exist") 1415 | time.Sleep(time.Second) 1416 | del := dnsEndpointList() 1417 | Expect(k8sClient.List(context.Background(), del, client.InNamespace(deNs))).ShouldNot(HaveOccurred()) 1418 | Expect(del.Items).Should(BeEmpty()) 1419 | }) 1420 | 1421 | It("should not create Certificate if the issuer namespace is not allowed", func() { 1422 | ns := testNamespacePrefix + randomString(10) 1423 | certNs := testNamespacePrefix + randomString(10) 1424 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1425 | ObjectMeta: ctrl.ObjectMeta{Name: ns}, 1426 | })).ShouldNot(HaveOccurred()) 1427 | Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ 1428 | ObjectMeta: ctrl.ObjectMeta{Name: certNs}, 1429 | })).ShouldNot(HaveOccurred()) 1430 | 1431 | scm, mgr := setupManager() 1432 | 1433 | Expect(SetupReconciler(mgr, scm, ReconcilerOptions{ 1434 | ServiceKey: testServiceKey, 1435 | CreateCertificate: true, 1436 | DefaultIssuerKind: IssuerKind, 1437 | DefaultIssuerName: "test-issuer", 1438 | AllowedIssuerNamespaces: []string{}, 1439 | })).ShouldNot(HaveOccurred()) 1440 | 1441 | stopMgr := startTestManager(mgr) 1442 | defer stopMgr() 1443 | 1444 | By("creating HTTPProxy with Certificate namespace annotation") 1445 | hpKey := client.ObjectKey{Name: "foo", Namespace: ns} 1446 | hp := newDummyHTTPProxy(hpKey) 1447 | hp.Spec.VirtualHost.TLS = nil 1448 | hp.Annotations[issuerNamespaceAnnotation] = certNs 1449 | Expect(k8sClient.Create(context.Background(), hp)).ShouldNot(HaveOccurred()) 1450 | 1451 | By("confirming that Certificate does not exist") 1452 | time.Sleep(time.Second) 1453 | crtList := certificateList() 1454 | Expect(k8sClient.List(context.Background(), crtList, client.InNamespace(certNs))).ShouldNot(HaveOccurred()) 1455 | Expect(crtList.Items).Should(BeEmpty()) 1456 | 1457 | By("confirming that TLSCertificateDelegation does not exist") 1458 | tcdList := tlsCertificateDelegationList() 1459 | Expect(k8sClient.List(context.Background(), tcdList, client.InNamespace(certNs))).ShouldNot(HaveOccurred()) 1460 | Expect(tcdList.Items).Should(BeEmpty()) 1461 | }) 1462 | } 1463 | 1464 | func newDummyHTTPProxy(hpKey client.ObjectKey) *projectcontourv1.HTTPProxy { 1465 | return &projectcontourv1.HTTPProxy{ 1466 | ObjectMeta: v1.ObjectMeta{ 1467 | Namespace: hpKey.Namespace, 1468 | Name: hpKey.Name, 1469 | Annotations: map[string]string{ 1470 | testACMETLSAnnotation: "true", 1471 | }, 1472 | }, 1473 | Spec: projectcontourv1.HTTPProxySpec{ 1474 | VirtualHost: &projectcontourv1.VirtualHost{ 1475 | Fqdn: dnsName, 1476 | TLS: &projectcontourv1.TLS{SecretName: testSecretName}, 1477 | }, 1478 | Routes: []projectcontourv1.Route{}, 1479 | }, 1480 | } 1481 | } 1482 | 1483 | func TestIsClassNameMatched(t *testing.T) { 1484 | tests := []struct { 1485 | name string 1486 | ingressClassName string 1487 | hp *projectcontourv1.HTTPProxy 1488 | want bool 1489 | }{ 1490 | { 1491 | name: "Annotation is not set", 1492 | ingressClassName: "class-name", 1493 | hp: &projectcontourv1.HTTPProxy{ 1494 | ObjectMeta: v1.ObjectMeta{ 1495 | Annotations: map[string]string{}, 1496 | }, 1497 | }, 1498 | want: false, 1499 | }, 1500 | { 1501 | name: "Spec is not set", 1502 | ingressClassName: "class-name", 1503 | hp: &projectcontourv1.HTTPProxy{ 1504 | ObjectMeta: v1.ObjectMeta{ 1505 | Annotations: map[string]string{}, 1506 | }, 1507 | Spec: projectcontourv1.HTTPProxySpec{}, 1508 | }, 1509 | want: false, 1510 | }, 1511 | { 1512 | name: "Both annotation are set", 1513 | ingressClassName: "class-name", 1514 | hp: &projectcontourv1.HTTPProxy{ 1515 | ObjectMeta: v1.ObjectMeta{ 1516 | Annotations: map[string]string{ 1517 | ingressClassNameAnnotation: "class-name", 1518 | contourIngressClassNameAnnotation: "class-name", 1519 | }, 1520 | }, 1521 | }, 1522 | want: true, 1523 | }, 1524 | { 1525 | name: "Both annotation and spec are set", 1526 | ingressClassName: "class-name", 1527 | hp: &projectcontourv1.HTTPProxy{ 1528 | ObjectMeta: v1.ObjectMeta{ 1529 | Annotations: map[string]string{ 1530 | ingressClassNameAnnotation: "class-name", 1531 | contourIngressClassNameAnnotation: "class-name", 1532 | }, 1533 | }, 1534 | Spec: projectcontourv1.HTTPProxySpec{ 1535 | IngressClassName: "class-name", 1536 | }, 1537 | }, 1538 | want: true, 1539 | }, 1540 | { 1541 | name: "Both annotation are set but not matched", 1542 | ingressClassName: "class-name", 1543 | hp: &projectcontourv1.HTTPProxy{ 1544 | ObjectMeta: v1.ObjectMeta{ 1545 | Annotations: map[string]string{ 1546 | ingressClassNameAnnotation: "class-name", 1547 | contourIngressClassNameAnnotation: "wrong", 1548 | }, 1549 | }, 1550 | }, 1551 | want: false, 1552 | }, 1553 | { 1554 | name: "Both annotation and spec are set but not matched", 1555 | ingressClassName: "class-name", 1556 | hp: &projectcontourv1.HTTPProxy{ 1557 | ObjectMeta: v1.ObjectMeta{ 1558 | Annotations: map[string]string{ 1559 | ingressClassNameAnnotation: "class-name", 1560 | contourIngressClassNameAnnotation: "wrong", 1561 | }, 1562 | }, 1563 | Spec: projectcontourv1.HTTPProxySpec{ 1564 | IngressClassName: "class-name", 1565 | }, 1566 | }, 1567 | want: false, 1568 | }, 1569 | { 1570 | name: fmt.Sprintf("Annotation %s is set", ingressClassNameAnnotation), 1571 | ingressClassName: "class-name", 1572 | hp: &projectcontourv1.HTTPProxy{ 1573 | ObjectMeta: v1.ObjectMeta{ 1574 | Annotations: map[string]string{ 1575 | ingressClassNameAnnotation: "class-name", 1576 | }, 1577 | }, 1578 | }, 1579 | want: true, 1580 | }, 1581 | { 1582 | name: fmt.Sprintf("Annotation %s is set but not matched", ingressClassNameAnnotation), 1583 | ingressClassName: "class-name", 1584 | hp: &projectcontourv1.HTTPProxy{ 1585 | ObjectMeta: v1.ObjectMeta{ 1586 | Annotations: map[string]string{ 1587 | ingressClassNameAnnotation: "wrong", 1588 | }, 1589 | }, 1590 | }, 1591 | want: false, 1592 | }, 1593 | { 1594 | name: fmt.Sprintf("Annotation %s is set", contourIngressClassNameAnnotation), 1595 | ingressClassName: "class-name", 1596 | hp: &projectcontourv1.HTTPProxy{ 1597 | ObjectMeta: v1.ObjectMeta{ 1598 | Annotations: map[string]string{ 1599 | contourIngressClassNameAnnotation: "class-name", 1600 | }, 1601 | }, 1602 | }, 1603 | want: true, 1604 | }, 1605 | { 1606 | name: fmt.Sprintf("Annotation %s is set but not matched", contourIngressClassNameAnnotation), 1607 | ingressClassName: "class-name", 1608 | hp: &projectcontourv1.HTTPProxy{ 1609 | ObjectMeta: v1.ObjectMeta{ 1610 | Annotations: map[string]string{ 1611 | contourIngressClassNameAnnotation: "wrong", 1612 | }, 1613 | }, 1614 | }, 1615 | want: false, 1616 | }, 1617 | { 1618 | name: "Spec is set", 1619 | ingressClassName: "class-name", 1620 | hp: &projectcontourv1.HTTPProxy{ 1621 | ObjectMeta: v1.ObjectMeta{ 1622 | Annotations: map[string]string{}, 1623 | }, 1624 | Spec: projectcontourv1.HTTPProxySpec{ 1625 | IngressClassName: "class-name", 1626 | }, 1627 | }, 1628 | want: true, 1629 | }, 1630 | { 1631 | name: "Spec is set but not matched", 1632 | ingressClassName: "class-name", 1633 | hp: &projectcontourv1.HTTPProxy{ 1634 | ObjectMeta: v1.ObjectMeta{ 1635 | Annotations: map[string]string{}, 1636 | }, 1637 | Spec: projectcontourv1.HTTPProxySpec{ 1638 | IngressClassName: "wrong", 1639 | }, 1640 | }, 1641 | want: false, 1642 | }, 1643 | } 1644 | for _, tt := range tests { 1645 | t.Run(tt.name, func(t *testing.T) { 1646 | r := &HTTPProxyReconciler{ 1647 | ReconcilerOptions: ReconcilerOptions{ 1648 | IngressClassName: tt.ingressClassName, 1649 | }, 1650 | } 1651 | if got := r.isClassNameMatched(tt.hp); got != tt.want { 1652 | t.Errorf("HTTPProxyReconciler.isClassNameMatched() = %v, want %v", got, tt.want) 1653 | } 1654 | }) 1655 | } 1656 | } 1657 | 1658 | func TestMakeDelegationEndpoint(t *testing.T) { 1659 | tests := []struct { 1660 | name string 1661 | hostname string 1662 | delegatedDomain string 1663 | expectDNSName string 1664 | expectTarget string 1665 | }{ 1666 | { 1667 | name: "Hostname without trailing dot", 1668 | hostname: "example.com", 1669 | delegatedDomain: "delegated.com", 1670 | expectDNSName: "_acme-challenge.example.com", 1671 | expectTarget: "_acme-challenge.example.com.delegated.com", 1672 | }, 1673 | { 1674 | name: "Fully-qualified domain name", 1675 | hostname: "example.com.", 1676 | delegatedDomain: "delegated.com", 1677 | expectDNSName: "_acme-challenge.example.com", 1678 | expectTarget: "_acme-challenge.example.com.delegated.com", 1679 | }, 1680 | } 1681 | for _, tc := range tests { 1682 | t.Run(tc.name, func(t *testing.T) { 1683 | actuals := makeDelegationEndpoint(tc.hostname, tc.delegatedDomain) 1684 | if len(actuals) != 1 { 1685 | t.Errorf("HTTPProxyReconciler.makeDelegationEndpoint() = %v, want 1 item", len(actuals)) 1686 | } 1687 | actual := actuals[0] 1688 | if actual["dnsName"] != tc.expectDNSName { 1689 | t.Errorf("HTTPProxyReconciler.makeDelegationEndpoint() dnsName = %v, want %v", actual["dnsName"], tc.expectDNSName) 1690 | } 1691 | actualTargets := actual["targets"].([]string) 1692 | if len(actualTargets) != 1 { 1693 | t.Errorf("HTTPProxyReconciler.makeDelegationEndpoint() targets = %v, want 1 item", len(actualTargets)) 1694 | } 1695 | if actualTargets[0] != tc.expectTarget { 1696 | t.Errorf("HTTPProxyReconciler.makeDelegationEndpoint() target = %v, want %v", actualTargets[0], tc.expectTarget) 1697 | } 1698 | }) 1699 | } 1700 | } 1701 | 1702 | func TestGetCertificateName(t *testing.T) { 1703 | tests := []struct { 1704 | name string 1705 | reconciler *HTTPProxyReconciler 1706 | proxy *projectcontourv1.HTTPProxy 1707 | expectName string 1708 | }{ 1709 | { 1710 | name: "Default name", 1711 | reconciler: &HTTPProxyReconciler{}, 1712 | proxy: &projectcontourv1.HTTPProxy{ 1713 | ObjectMeta: v1.ObjectMeta{ 1714 | Name: "foo", 1715 | Namespace: "bar", 1716 | }, 1717 | }, 1718 | expectName: "foo", 1719 | }, 1720 | { 1721 | name: "Default name with prefix", 1722 | reconciler: &HTTPProxyReconciler{ 1723 | ReconcilerOptions: ReconcilerOptions{ 1724 | Prefix: "prefix-", 1725 | }, 1726 | }, 1727 | proxy: &projectcontourv1.HTTPProxy{ 1728 | ObjectMeta: v1.ObjectMeta{ 1729 | Name: "foo", 1730 | Namespace: "bar", 1731 | }, 1732 | }, 1733 | expectName: "prefix-foo", 1734 | }, 1735 | { 1736 | name: "Namespaced name", 1737 | reconciler: &HTTPProxyReconciler{}, 1738 | proxy: &projectcontourv1.HTTPProxy{ 1739 | ObjectMeta: v1.ObjectMeta{ 1740 | Name: "foo", 1741 | Namespace: "bar", 1742 | Annotations: map[string]string{ 1743 | issuerNamespaceAnnotation: "custom-issuer", 1744 | }, 1745 | }, 1746 | }, 1747 | expectName: "bar-foo", 1748 | }, 1749 | { 1750 | name: "Namespaced name with prefix", 1751 | reconciler: &HTTPProxyReconciler{ 1752 | ReconcilerOptions: ReconcilerOptions{ 1753 | Prefix: "prefix-", 1754 | }, 1755 | }, 1756 | proxy: &projectcontourv1.HTTPProxy{ 1757 | ObjectMeta: v1.ObjectMeta{ 1758 | Name: "foo", 1759 | Namespace: "bar", 1760 | Annotations: map[string]string{ 1761 | issuerNamespaceAnnotation: "custom-issuer", 1762 | }, 1763 | }, 1764 | }, 1765 | expectName: "prefix-bar-foo", 1766 | }, 1767 | } 1768 | 1769 | for _, tc := range tests { 1770 | t.Run(tc.name, func(t *testing.T) { 1771 | actual := getCertificateName(tc.reconciler, tc.proxy) 1772 | if actual != tc.expectName { 1773 | t.Errorf("HTTPProxyReconciler.getCertificateName() = %v, want %v", actual, tc.expectName) 1774 | } 1775 | }) 1776 | } 1777 | } 1778 | 1779 | func TestGetDNSEndpointName(t *testing.T) { 1780 | tests := []struct { 1781 | name string 1782 | reconciler *HTTPProxyReconciler 1783 | proxy *projectcontourv1.HTTPProxy 1784 | expectName string 1785 | }{ 1786 | { 1787 | name: "Default name", 1788 | reconciler: &HTTPProxyReconciler{}, 1789 | proxy: &projectcontourv1.HTTPProxy{ 1790 | ObjectMeta: v1.ObjectMeta{ 1791 | Name: "foo", 1792 | Namespace: "bar", 1793 | }, 1794 | }, 1795 | expectName: "foo", 1796 | }, 1797 | { 1798 | name: "Default name with prefix", 1799 | reconciler: &HTTPProxyReconciler{ 1800 | ReconcilerOptions: ReconcilerOptions{ 1801 | Prefix: "prefix-", 1802 | }, 1803 | }, 1804 | proxy: &projectcontourv1.HTTPProxy{ 1805 | ObjectMeta: v1.ObjectMeta{ 1806 | Name: "foo", 1807 | Namespace: "bar", 1808 | }, 1809 | }, 1810 | expectName: "prefix-foo", 1811 | }, 1812 | { 1813 | name: "Namespaced name", 1814 | reconciler: &HTTPProxyReconciler{}, 1815 | proxy: &projectcontourv1.HTTPProxy{ 1816 | ObjectMeta: v1.ObjectMeta{ 1817 | Name: "foo", 1818 | Namespace: "bar", 1819 | Annotations: map[string]string{ 1820 | dnsNamespaceAnnotation: "custom-issuer", 1821 | }, 1822 | }, 1823 | }, 1824 | expectName: "bar-foo", 1825 | }, 1826 | { 1827 | name: "Namespaced name with prefix", 1828 | reconciler: &HTTPProxyReconciler{ 1829 | ReconcilerOptions: ReconcilerOptions{ 1830 | Prefix: "prefix-", 1831 | }, 1832 | }, 1833 | proxy: &projectcontourv1.HTTPProxy{ 1834 | ObjectMeta: v1.ObjectMeta{ 1835 | Name: "foo", 1836 | Namespace: "bar", 1837 | Annotations: map[string]string{ 1838 | dnsNamespaceAnnotation: "custom-issuer", 1839 | }, 1840 | }, 1841 | }, 1842 | expectName: "prefix-bar-foo", 1843 | }, 1844 | } 1845 | for _, tc := range tests { 1846 | t.Run(tc.name, func(t *testing.T) { 1847 | actual := getDNSEndpointName(tc.reconciler, tc.proxy) 1848 | if actual != tc.expectName { 1849 | t.Errorf("HTTPProxyReconciler.getDNSEndpointName() = %v, want %v", actual, tc.expectName) 1850 | } 1851 | }) 1852 | } 1853 | } 1854 | --------------------------------------------------------------------------------