├── .dockerignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE ├── dependabot.yaml ├── release.yaml └── workflows │ ├── backport.yaml │ ├── docker-main.yaml │ ├── docker-version-branches.yaml │ ├── docs.yaml │ ├── lint.yml │ ├── release.yaml │ ├── test.yaml │ └── update-core.yaml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── Dockerfile ├── Dockerfile.ci ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── apis ├── gateway │ └── v1alpha1 │ │ ├── filter_types.go │ │ ├── groupversion_info.go │ │ └── zz_generated.deepcopy.go └── ingress │ └── v1 │ ├── deprecation.go │ ├── deprecation_test.go │ ├── groupversion_info.go │ ├── pomerium_types.go │ └── zz_generated.deepcopy.go ├── changelog └── v0.29.0.md ├── cmd ├── all_in_one.go ├── common.go ├── controller.go ├── controller_test.go ├── gen_secrets.go ├── ingress_opts.go └── root.go ├── config ├── .gitignore ├── crd │ ├── bases │ │ ├── gateway.pomerium.io_policyfilters.yaml │ │ └── ingress.pomerium.io_pomerium.yaml │ ├── files.go │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── default │ └── kustomization.yaml ├── gateway-api │ ├── gatewayclass.yaml │ ├── kustomization.yaml │ └── role_patch.yaml ├── gen_secrets │ ├── job.yaml │ ├── kustomization.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── pomerium │ ├── deployment │ │ ├── args.yaml │ │ ├── base.yaml │ │ ├── healthcheck.yaml │ │ ├── image.yaml │ │ ├── kustomization.yaml │ │ ├── no-root.yaml │ │ ├── ports.yaml │ │ ├── readonly-root-fs.yaml │ │ └── resources.yaml │ ├── ingressclass.yaml │ ├── kustomization.yaml │ ├── namespace.yaml │ ├── rbac │ │ ├── kustomization.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ └── service_account.yaml │ └── service │ │ ├── kustomization.yaml │ │ ├── metrics.yaml │ │ └── proxy.yaml ├── prometheus │ ├── coreos-podmonitor.yaml │ └── gke-podmonitor.yaml └── stress-test │ ├── config.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── namespace.yaml │ ├── rbac │ ├── kustomization.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml │ └── service.yaml ├── controllers ├── config_controller.go ├── deps │ ├── deps.go │ └── registry_client.go ├── gateway │ ├── conditions.go │ ├── controller.go │ ├── extensionfilters.go │ ├── fetch.go │ ├── gateway.go │ ├── gatewayclass.go │ ├── httproute.go │ ├── listener.go │ ├── referencegrant.go │ └── refkey.go ├── ingress │ ├── builder.go │ ├── controller.go │ ├── controller_integration_test.go │ ├── controller_test.go │ ├── deps.go │ ├── fetch.go │ ├── ingress_class.go │ ├── once.go │ ├── once_test.go │ └── reconcile.go ├── mock │ ├── client.go │ ├── mock.go │ ├── pomerium_config_reconciler.go │ └── pomerium_ingress_reconciler.go ├── reporter │ ├── ingress.go │ ├── pomerium.go │ └── reporter.go └── settings │ ├── controller.go │ ├── fetch.go │ ├── fetch_test.go │ └── integration_test.go ├── cspell.config.yaml ├── deployment.yaml ├── docs ├── cmd │ └── main.go ├── docs.go ├── known_formats.go ├── parse.go ├── template.go └── templates │ ├── header.tmpl │ ├── object-properties.tmpl │ ├── object.tmpl │ └── objects.tmpl ├── go.mod ├── go.sum ├── internal ├── .gitignore ├── filemgr │ ├── filemgr.go │ └── filemgr_test.go ├── init.go ├── init_embed.go └── stress │ ├── cmd │ └── command.go │ ├── echo.go │ ├── ingress.go │ └── traffic.go ├── main.go ├── model ├── gateway_config.go ├── ingress_config.go ├── registry.go └── registry_test.go ├── pomerium ├── cert_map.go ├── certs.go ├── config.go ├── config_test.go ├── ctrl │ ├── bootstrap.go │ ├── bootstrap_test.go │ ├── config.go │ ├── config_test.go │ └── run.go ├── envoy │ ├── envoy_darwin_amd64.go │ ├── envoy_darwin_arm64.go │ ├── envoy_linux_amd64.go │ ├── envoy_linux_arm64.go │ ├── envoy_test.go │ ├── validate.go │ ├── validate_envoy.go │ └── validate_noop.go ├── gateway │ ├── backendrefs.go │ ├── filters.go │ ├── matches.go │ └── translate.go ├── ingress_annotations.go ├── ingress_annotations_test.go ├── ingress_to_route.go ├── proto.go ├── reconcile.go ├── route_list.go ├── routes.go ├── routes_test.go ├── sync.go └── validate.go ├── reference.md ├── scripts ├── check-docker-images ├── check-image-tag.sh └── open-docs-pull-request.sh └── util ├── bin.go ├── bin_test.go ├── generic ├── builder.go ├── doc.go └── gvk.go ├── merge_map.go ├── merge_map_test.go ├── namespaced_name.go ├── namespaced_name_test.go ├── restart.go ├── restart_test.go └── secrets.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore all files which are not go type 3 | !**/*.go 4 | !**/*.mod 5 | !**/*.sum 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pomerium/dev-backend 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know about a bug! 4 | --- 5 | 6 | ## What happened? 7 | 8 | ## What did you expect to happen? 9 | 10 | ## How'd it happen? 11 | 12 | 1. Ran `x` 13 | 2. Clicked `y` 14 | 3. Saw error `z` 15 | 16 | ## What's your environment like? 17 | 18 | - Pomerium version (retrieve with `pomerium --version`): 19 | - Server Operating System/Architecture/Cloud: 20 | 21 | ## What's your config.yaml? 22 | 23 | ```config.yaml 24 | # Paste your configs here 25 | # Be sure to scrub any sensitive values 26 | ``` 27 | 28 | ## What did you see in the logs? 29 | 30 | ```logs 31 | # Paste your logs here. 32 | # Be sure to scrub any sensitive values 33 | ``` 34 | 35 | ## Additional context 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest something! 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | 12 | **Describe alternatives you've considered** 13 | 14 | **Explain any additional use-cases** 15 | 16 | If there are any use-cases that would help us understand the use/need/value please share them as they can help us decide on acceptance and prioritization. 17 | 18 | **Additional context** 19 | 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 10 | 11 | ## Related issues 12 | 13 | 16 | 17 | 18 | ## Checklist 19 | 20 | - [ ] reference any related issues 21 | - [ ] updated docs 22 | - [ ] updated unit tests 23 | - [ ] updated UPGRADING.md 24 | - [ ] add appropriate tag (`improvement` / `bug` / etc) 25 | - [ ] ready for review 26 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "docker" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 50 8 | groups: 9 | docker: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | open-pull-requests-limit: 50 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | - package-ecosystem: "gomod" 22 | directory: "/" 23 | schedule: 24 | interval: "monthly" 25 | open-pull-requests-limit: 50 26 | ignore: 27 | - dependency-name: "github.com/pomerium/pomerium" 28 | groups: 29 | go: 30 | patterns: 31 | - "*" 32 | exclude-patterns: 33 | - "*k8s.io*" 34 | k8s: 35 | patterns: 36 | - "*k8s.io*" 37 | -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ci 5 | - ignore-changelog 6 | categories: 7 | - title: Breaking 8 | labels: 9 | - breaking 10 | - title: Security 11 | labels: 12 | - security 13 | - title: New 14 | labels: 15 | - enhancement 16 | - feature 17 | - title: Fixes 18 | labels: 19 | - bug 20 | - title: Changed 21 | labels: 22 | - "*" 23 | exclude: 24 | labels: 25 | - dependencies 26 | - title: Dependency Updates 27 | labels: 28 | - dependencies 29 | -------------------------------------------------------------------------------- /.github/workflows/backport.yaml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | permissions: 3 | contents: read 4 | on: 5 | pull_request_target: 6 | types: 7 | - closed 8 | - labeled 9 | 10 | jobs: 11 | backport: 12 | runs-on: ubuntu-latest 13 | name: Backport 14 | steps: 15 | - name: Generate token 16 | id: generate_token 17 | uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a 18 | with: 19 | app_id: ${{ secrets.BACKPORT_APP_APPID }} 20 | private_key: ${{ secrets.BACKPORT_APP_PRIVATE_KEY }} 21 | 22 | - name: Backport 23 | uses: pomerium/backport@e2ffd4c5a70730dfd19046859dfaf366e3de6466 24 | with: 25 | github_token: ${{ steps.generate_token.outputs.token }} 26 | title_template: "{{originalTitle}}" 27 | copy_original_labels: true 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-main.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Main 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 20 | with: 21 | go-version: 1.24.x 22 | 23 | - name: Docker meta 24 | id: meta 25 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 26 | with: 27 | # list of Docker images to use as base name for tags 28 | images: | 29 | pomerium/ingress-controller 30 | # generate Docker tags based on the following events/attributes 31 | tags: | 32 | type=ref,event=branch 33 | type=sha 34 | 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 37 | 38 | - name: Login to DockerHub 39 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 40 | with: 41 | username: ${{ secrets.DOCKERHUB_USER }} 42 | password: ${{ secrets.DOCKERHUB_TOKEN }} 43 | 44 | - name: Build 45 | run: make build-ci 46 | 47 | - name: Docker Publish - Main 48 | uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 49 | with: 50 | context: . 51 | file: ./Dockerfile.ci 52 | push: true 53 | platforms: linux/amd64,linux/arm64 54 | tags: ${{ steps.meta.outputs.tags }} 55 | labels: ${{ steps.meta.outputs.labels }} 56 | -------------------------------------------------------------------------------- /.github/workflows/docker-version-branches.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Release Branches 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - "[0-9]+-[0-9]+-*" 8 | - "experimental/*" 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Go 20 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 21 | with: 22 | go-version: 1.24.x 23 | 24 | - name: Docker meta 25 | id: meta 26 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 27 | with: 28 | # list of Docker images to use as base name for tags 29 | images: | 30 | pomerium/ingress-controller 31 | # generate Docker tags based on the following events/attributes 32 | tags: | 33 | type=ref,event=branch 34 | type=sha 35 | 36 | - name: Set up QEMU 37 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 38 | 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 41 | 42 | - name: Login to DockerHub 43 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 44 | with: 45 | username: ${{ secrets.DOCKERHUB_USER }} 46 | password: ${{ secrets.DOCKERHUB_TOKEN }} 47 | 48 | - name: Build 49 | run: make build-ci 50 | 51 | - name: Docker Publish - Version Branches 52 | uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 53 | with: 54 | context: . 55 | file: ./Dockerfile.ci 56 | push: true 57 | platforms: linux/amd64,linux/arm64 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | pull-request: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 13 | 14 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 15 | with: 16 | go-version: 1.24.x 17 | 18 | - name: generate docs 19 | run: make docs 20 | 21 | - name: Create pull request in the documentations repo 22 | env: 23 | API_TOKEN_GITHUB: ${{ secrets.APPARITOR_GITHUB_TOKEN }} 24 | USER_EMAIL: ${{ github.event.pusher.email }} 25 | USER_NAME: ${{ github.event.pusher.name }} 26 | run: scripts/open-docs-pull-request.sh 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: {} 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 19 | with: 20 | go-version: 1.24.x 21 | cache: false 22 | 23 | - run: make envoy 24 | - run: make pomerium-ui 25 | 26 | - name: Run golangci-lint 27 | uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 28 | with: 29 | version: v1.64.8 30 | args: --timeout=10m 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Check image reference 20 | # check that docker image reference is the same as the release tag 21 | run: ./scripts/check-image-tag.sh ${{ github.event.release.tag_name }} 22 | 23 | - name: Docker meta 24 | id: meta 25 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 26 | with: 27 | images: | 28 | pomerium/ingress-controller 29 | tags: | 30 | type=semver,pattern={{raw}} 31 | type=semver,pattern=v{{major}}.{{minor}} 32 | type=sha 33 | 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 36 | 37 | - name: Login to DockerHub 38 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USER }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | 43 | - name: Setup Go 44 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 45 | with: 46 | go-version: 1.24.x 47 | 48 | - name: Build 49 | run: make build-ci 50 | 51 | - name: Docker Publish - Main 52 | uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 53 | with: 54 | context: . 55 | file: ./Dockerfile.ci 56 | push: true 57 | platforms: linux/amd64,linux/arm64 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | pre-commit: 12 | runs-on: ubuntu-latest 13 | if: github.event_name == 'pull_request' 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 19 | with: 20 | go-version: 1.24.x 21 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 22 | with: 23 | python-version: "3.x" 24 | - name: install kustomize 25 | run: make kustomize 26 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd 27 | with: 28 | extra_args: --show-diff-on-failure --from-ref ${{ 29 | github.event.pull_request.base.sha }} --to-ref ${{ 30 | github.event.pull_request.head.sha }} 31 | env: 32 | SKIP: go-mod-tidy,lint 33 | 34 | test: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 38 | with: 39 | fetch-depth: 0 40 | 41 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 42 | with: 43 | go-version: 1.24.x 44 | 45 | - name: set env vars 46 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 47 | 48 | - name: test 49 | if: runner.os == 'Linux' 50 | run: make test 51 | env: 52 | # Remove this override when 1.31.1 is available for envtest 53 | ENVTEST_K8S_VERSION: 1.31.0 54 | 55 | build: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 59 | 60 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 61 | with: 62 | go-version: 1.24.x 63 | 64 | - name: build 65 | run: make build 66 | -------------------------------------------------------------------------------- /.github/workflows/update-core.yaml: -------------------------------------------------------------------------------- 1 | name: Update Core to Latest Commit 2 | 3 | on: 4 | schedule: 5 | - cron: "40 1 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-pomerium-core: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 14 | - name: Setup Go 15 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 16 | with: 17 | go-version: 1.24.x 18 | - name: Update Core 19 | run: | 20 | go get -u github.com/pomerium/pomerium@main 21 | go mod tidy 22 | - name: Check for changes 23 | id: git-diff 24 | run: | 25 | git config --global user.email "apparitor@users.noreply.github.com" 26 | git config --global user.name "GitHub Actions" 27 | git add go.mod go.sum 28 | git diff --cached --exit-code || echo "changed=true" >> $GITHUB_OUTPUT 29 | 30 | - name: Create Pull Request 31 | if: ${{ steps.git-diff.outputs.changed }} == 'true' 32 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e 33 | with: 34 | author: GitHub Actions 35 | body: "This PR updates the Pomerium Core to the latest commit in main" 36 | branch: ci/update-core 37 | commit-message: "ci: update core to latest commit in main" 38 | delete-branch: true 39 | labels: ci 40 | title: "ci: update core to latest commit in main" 41 | token: ${{ secrets.APPARITOR_GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | testbin/* 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | 26 | dist/ 27 | .vscode/ 28 | 29 | .DS_Store 30 | 31 | .worktrees/ 32 | 33 | changelog.md 34 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | exclude: "(integration/tpl/files/.*)" 8 | - id: check-yaml 9 | exclude: "deployment.yaml" 10 | - id: check-added-large-files 11 | - repo: https://github.com/syntaqx/git-hooks 12 | rev: v0.0.17 13 | hooks: 14 | - id: go-mod-tidy 15 | - repo: https://github.com/streetsidesoftware/cspell-cli 16 | rev: v6.17.1 17 | hooks: 18 | - id: cspell 19 | files: "^.*.go$" 20 | - repo: local 21 | hooks: 22 | - id: lint 23 | name: lint 24 | language: system 25 | entry: make 26 | args: ["lint"] 27 | types: ["go"] 28 | pass_filenames: false 29 | fail_fast: true 30 | - id: deployment 31 | name: deployment 32 | fail_fast: true 33 | language: system 34 | entry: make 35 | args: ["deployment"] 36 | types: ["yaml"] 37 | pass_filenames: false 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use distroless as minimal base image to package the manager binary 2 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 3 | FROM gcr.io/distroless/base-debian12:debug-nonroot@sha256:76acc040228aed628435f9951e0bee94f99645efabcdf362e94a8c70ba422f99 4 | COPY bin/manager /manager 5 | USER 65532:65532 6 | 7 | ENTRYPOINT ["/manager"] 8 | -------------------------------------------------------------------------------- /Dockerfile.ci: -------------------------------------------------------------------------------- 1 | # Use distroless as minimal base image to package the manager binary 2 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 3 | FROM gcr.io/distroless/base-debian12:debug-nonroot@sha256:76acc040228aed628435f9951e0bee94f99645efabcdf362e94a8c70ba422f99 4 | ARG TARGETARCH 5 | COPY bin/manager-linux-$TARGETARCH /manager 6 | USER 65532:65532 7 | 8 | ENTRYPOINT ["/manager"] 9 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: pomerium.io 2 | layout: 3 | - go.kubebuilder.io/v3 4 | multigroup: true 5 | projectName: ingress-controller 6 | repo: github.com/pomerium/ingress-controller 7 | resources: 8 | - controller: true 9 | domain: networking.k8s.io 10 | kind: Ingress 11 | version: v1 12 | - api: 13 | crdVersion: v1 14 | namespaced: false 15 | domain: pomerium.io 16 | group: ingress 17 | kind: Pomerium 18 | plural: pomerium 19 | path: github.com/pomerium/ingress-controller/apis/ingress/v1 20 | version: v1 21 | version: "3" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pomerium for Kubernetes 2 | 3 | Use Pomerium as a first-class secure-by-default Ingress Controller. The Pomerium Ingress Controller enables workflows more native to Kubernetes environments, such as Git-Ops style actions based on pull requests. Dynamically provision routes from Ingress resources and set policy based on annotations. By defining routes as Ingress resources you can independently create and remove them from Pomerium's configuration. 4 | 5 | # Docs 6 | 7 | - [Install Pomerium](https://www.pomerium.com/docs/k8s/install). 8 | - [Global Configuration](https://www.pomerium.com/docs/k8s/configure). 9 | - [Ingress Configuration](https://www.pomerium.com/docs/k8s/ingress). 10 | - [Pomerium CRD Reference](https://www.pomerium.com/docs/k8s/reference). 11 | -------------------------------------------------------------------------------- /apis/gateway/v1alpha1/filter_types.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains custom resource definitions for use with the Gateway API. 2 | package v1alpha1 3 | 4 | import ( 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // PolicyFilter represents a Pomerium policy that can be attached to a particular route defined 9 | // via the Kubernetes Gateway API. 10 | // 11 | // +kubebuilder:object:root=true 12 | // +kubebuilder:subresource:status 13 | type PolicyFilter struct { 14 | metav1.TypeMeta `json:",inline"` 15 | metav1.ObjectMeta `json:"metadata,omitempty"` 16 | 17 | // Spec defines the content of the policy. 18 | Spec PolicyFilterSpec `json:"spec,omitempty"` 19 | 20 | // Status contains the status of the policy (e.g. is the policy valid). 21 | Status PolicyFilterStatus `json:"status,omitempty"` 22 | } 23 | 24 | // PolicyFilterSpec defines policy rules. 25 | type PolicyFilterSpec struct { 26 | // Policy rules in Pomerium Policy Language (PPL) syntax. May be expressed 27 | // in either YAML or JSON format. 28 | PPL string `json:"ppl,omitempty"` 29 | } 30 | 31 | // PolicyFilterStatus represents the state of a PolicyFilter. 32 | type PolicyFilterStatus struct { 33 | // Conditions describe the current state of the PolicyFilter. 34 | // 35 | // +optional 36 | // +listType=map 37 | // +listMapKey=type 38 | Conditions []metav1.Condition `json:"conditions,omitempty"` 39 | } 40 | 41 | //+kubebuilder:object:root=true 42 | 43 | // PolicyFilterList is a list of PolicyFilters. 44 | type PolicyFilterList struct { 45 | metav1.TypeMeta `json:",inline"` 46 | metav1.ListMeta `json:"metadata,omitempty"` 47 | Items []PolicyFilter `json:"items"` 48 | } 49 | 50 | func init() { 51 | SchemeBuilder.Register(&PolicyFilter{}, &PolicyFilterList{}) 52 | } 53 | -------------------------------------------------------------------------------- /apis/gateway/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the gateway v1alpha1 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=gateway.pomerium.io 4 | package v1alpha1 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects 13 | GroupVersion = schema.GroupVersion{Group: "gateway.pomerium.io", Version: "v1alpha1"} 14 | 15 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 16 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 17 | 18 | // AddToScheme adds the types in this group-version to the given scheme. 19 | AddToScheme = SchemeBuilder.AddToScheme 20 | ) 21 | -------------------------------------------------------------------------------- /apis/gateway/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // Code generated by controller-gen. DO NOT EDIT. 4 | 5 | package v1alpha1 6 | 7 | import ( 8 | "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | runtime "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 13 | func (in *PolicyFilter) DeepCopyInto(out *PolicyFilter) { 14 | *out = *in 15 | out.TypeMeta = in.TypeMeta 16 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 17 | out.Spec = in.Spec 18 | in.Status.DeepCopyInto(&out.Status) 19 | } 20 | 21 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyFilter. 22 | func (in *PolicyFilter) DeepCopy() *PolicyFilter { 23 | if in == nil { 24 | return nil 25 | } 26 | out := new(PolicyFilter) 27 | in.DeepCopyInto(out) 28 | return out 29 | } 30 | 31 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 32 | func (in *PolicyFilter) DeepCopyObject() runtime.Object { 33 | if c := in.DeepCopy(); c != nil { 34 | return c 35 | } 36 | return nil 37 | } 38 | 39 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 40 | func (in *PolicyFilterList) DeepCopyInto(out *PolicyFilterList) { 41 | *out = *in 42 | out.TypeMeta = in.TypeMeta 43 | in.ListMeta.DeepCopyInto(&out.ListMeta) 44 | if in.Items != nil { 45 | in, out := &in.Items, &out.Items 46 | *out = make([]PolicyFilter, len(*in)) 47 | for i := range *in { 48 | (*in)[i].DeepCopyInto(&(*out)[i]) 49 | } 50 | } 51 | } 52 | 53 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyFilterList. 54 | func (in *PolicyFilterList) DeepCopy() *PolicyFilterList { 55 | if in == nil { 56 | return nil 57 | } 58 | out := new(PolicyFilterList) 59 | in.DeepCopyInto(out) 60 | return out 61 | } 62 | 63 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 64 | func (in *PolicyFilterList) DeepCopyObject() runtime.Object { 65 | if c := in.DeepCopy(); c != nil { 66 | return c 67 | } 68 | return nil 69 | } 70 | 71 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 72 | func (in *PolicyFilterSpec) DeepCopyInto(out *PolicyFilterSpec) { 73 | *out = *in 74 | } 75 | 76 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyFilterSpec. 77 | func (in *PolicyFilterSpec) DeepCopy() *PolicyFilterSpec { 78 | if in == nil { 79 | return nil 80 | } 81 | out := new(PolicyFilterSpec) 82 | in.DeepCopyInto(out) 83 | return out 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *PolicyFilterStatus) DeepCopyInto(out *PolicyFilterStatus) { 88 | *out = *in 89 | if in.Conditions != nil { 90 | in, out := &in.Conditions, &out.Conditions 91 | *out = make([]v1.Condition, len(*in)) 92 | for i := range *in { 93 | (*in)[i].DeepCopyInto(&(*out)[i]) 94 | } 95 | } 96 | } 97 | 98 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyFilterStatus. 99 | func (in *PolicyFilterStatus) DeepCopy() *PolicyFilterStatus { 100 | if in == nil { 101 | return nil 102 | } 103 | out := new(PolicyFilterStatus) 104 | in.DeepCopyInto(out) 105 | return out 106 | } 107 | -------------------------------------------------------------------------------- /apis/ingress/v1/deprecation.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | pom_cfg "github.com/pomerium/pomerium/config" 9 | 10 | "github.com/iancoleman/strcase" 11 | ) 12 | 13 | var deprecatedFields = map[string]pom_cfg.FieldMsg{ 14 | "idp_directory_sync": { 15 | DocsURL: "https://docs.pomerium.com/docs/overview/upgrading#idp-directory-sync", 16 | FieldCheckMsg: pom_cfg.FieldCheckMsgRemoved, 17 | KeyAction: pom_cfg.KeyActionWarn, 18 | }, 19 | } 20 | 21 | // GetDeprecations returns deprecation warnings 22 | func GetDeprecations(spec *PomeriumSpec) ([]pom_cfg.FieldMsg, error) { 23 | return getStructDeprecations(reflect.ValueOf(spec)) 24 | } 25 | 26 | func getFieldDeprecations(field reflect.StructField) (*pom_cfg.FieldMsg, error) { 27 | reason, ok := field.Tag.Lookup("deprecated") 28 | if !ok { 29 | return nil, nil 30 | } 31 | msg, ok := deprecatedFields[reason] 32 | if !ok { 33 | return nil, fmt.Errorf("%s: not found in the lookup", reason) 34 | } 35 | jsonKey, ok := field.Tag.Lookup("json") 36 | if !ok { 37 | jsonKey = strcase.ToLowerCamel(field.Name) 38 | } 39 | jsonKey = strings.Split(jsonKey, ",")[0] 40 | msg.Key = jsonKey 41 | return &msg, nil 42 | } 43 | 44 | func getStructDeprecations(val reflect.Value) ([]pom_cfg.FieldMsg, error) { 45 | val = reflect.Indirect(val) 46 | if !val.IsValid() || val.IsZero() || val.Kind() != reflect.Struct { 47 | return nil, nil 48 | } 49 | 50 | var out []pom_cfg.FieldMsg 51 | for _, field := range reflect.VisibleFields(val.Type()) { 52 | fieldVal := reflect.Indirect(val.FieldByIndex(field.Index)) 53 | if !fieldVal.IsValid() || fieldVal.IsZero() { 54 | continue 55 | } 56 | 57 | if fieldVal.Kind() == reflect.Struct { 58 | msgs, err := getStructDeprecations(fieldVal) 59 | if err != nil { 60 | return nil, fmt.Errorf("%s: %w", fieldVal.Type().Name(), err) 61 | } 62 | out = append(out, msgs...) 63 | } 64 | msg, err := getFieldDeprecations(field) 65 | if err != nil { 66 | return nil, fmt.Errorf("%s: %w", field.Name, err) 67 | } 68 | if msg != nil { 69 | out = append(out, *msg) 70 | } 71 | } 72 | 73 | return out, nil 74 | } 75 | -------------------------------------------------------------------------------- /apis/ingress/v1/deprecation_test.go: -------------------------------------------------------------------------------- 1 | package v1_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | "google.golang.org/protobuf/proto" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | api "github.com/pomerium/ingress-controller/apis/ingress/v1" 12 | ) 13 | 14 | func TestDeprecations(t *testing.T) { 15 | msgs, err := api.GetDeprecations(&api.PomeriumSpec{ 16 | Authenticate: new(api.Authenticate), 17 | IdentityProvider: &api.IdentityProvider{ 18 | Provider: "google", URL: proto.String("http://google.com"), 19 | ServiceAccountFromSecret: proto.String("secret"), 20 | RefreshDirectory: &api.RefreshDirectorySettings{ 21 | Interval: v1.Duration{Duration: time.Minute}, 22 | Timeout: v1.Duration{Duration: time.Minute}, 23 | }, 24 | }, 25 | Certificates: []string{}, 26 | Secrets: "", 27 | }) 28 | require.NoError(t, err) 29 | require.Len(t, msgs, 2) 30 | } 31 | -------------------------------------------------------------------------------- /apis/ingress/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the ingress v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=ingress.pomerium.io 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "ingress.pomerium.io", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | // Package cmd implements top level commands 2 | package cmd 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/hashicorp/go-multierror" 11 | "github.com/iancoleman/strcase" 12 | "github.com/spf13/pflag" 13 | "github.com/spf13/viper" 14 | "go.uber.org/zap/zapcore" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apiserver/pkg/server/healthz" 17 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 20 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 21 | gateway_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 22 | 23 | icgv1alpha1 "github.com/pomerium/ingress-controller/apis/gateway/v1alpha1" 24 | icsv1 "github.com/pomerium/ingress-controller/apis/ingress/v1" 25 | ) 26 | 27 | const ( 28 | defaultGRPCTimeout = time.Minute 29 | ) 30 | 31 | const ( 32 | webhookPort = "webhook-port" 33 | metricsBindAddress = "metrics-bind-address" 34 | healthProbeBindAddress = "health-probe-bind-address" 35 | ) 36 | 37 | func envName(name string) string { 38 | return strcase.ToScreamingSnake(name) 39 | } 40 | 41 | func setupLogger(debug bool) { 42 | level := zapcore.InfoLevel 43 | if debug { 44 | level = zapcore.DebugLevel 45 | } 46 | opts := zap.Options{ 47 | Level: level, 48 | StacktraceLevel: zapcore.DPanicLevel, 49 | } 50 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 51 | } 52 | 53 | func getScheme() (*runtime.Scheme, error) { 54 | scheme := runtime.NewScheme() 55 | for _, apply := range []struct { 56 | name string 57 | fn func(*runtime.Scheme) error 58 | }{ 59 | {"core", clientgoscheme.AddToScheme}, 60 | {"settings", icsv1.AddToScheme}, 61 | {"gateway_v1", gateway_v1.Install}, 62 | {"gateway_v1beta1", gateway_v1beta1.Install}, 63 | {"gateway.pomerium.io", icgv1alpha1.AddToScheme}, 64 | } { 65 | if err := apply.fn(scheme); err != nil { 66 | return nil, fmt.Errorf("%s: %w", apply.name, err) 67 | } 68 | } 69 | return scheme, nil 70 | } 71 | 72 | func viperWalk(flags *pflag.FlagSet) error { 73 | v := viper.New() 74 | var errs *multierror.Error 75 | flags.VisitAll(func(f *pflag.Flag) { 76 | if err := v.BindEnv(f.Name, envName(f.Name)); err != nil { 77 | errs = multierror.Append(errs, err) 78 | return 79 | } 80 | 81 | if !f.Changed && v.IsSet(f.Name) { 82 | val := v.Get(f.Name) 83 | errs = multierror.Append(errs, flags.Set(f.Name, fmt.Sprintf("%v", val))) 84 | } 85 | }) 86 | return errs.ErrorOrNil() 87 | } 88 | 89 | func runHealthz(ctx context.Context, addr string, readyChecks ...healthz.HealthChecker) error { 90 | ctx, cancel := context.WithCancel(ctx) 91 | defer cancel() 92 | 93 | mux := http.NewServeMux() 94 | healthz.InstallHandler(mux) 95 | healthz.InstallReadyzHandler(mux, readyChecks...) 96 | 97 | srv := http.Server{ 98 | Addr: addr, 99 | Handler: mux, 100 | ReadHeaderTimeout: time.Millisecond * 100, 101 | } 102 | go func() { 103 | <-ctx.Done() 104 | _ = srv.Close() 105 | }() 106 | 107 | return srv.ListenAndServe() 108 | } 109 | -------------------------------------------------------------------------------- /cmd/controller_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFlags(t *testing.T) { 12 | cmd := new(controllerCmd) 13 | caString := "pvsuDLZHrTr0vDt6+5ghiQ==" 14 | caData, err := base64.StdEncoding.DecodeString(caString) 15 | assert.NoError(t, err) 16 | for k, v := range map[string]string{ 17 | metricsBindAddress: ":5678", 18 | healthProbeBindAddress: ":9876", 19 | ingressClassControllerName: "class-name", 20 | annotationPrefix: "prefix", 21 | databrokerServiceURL: "https://host.somewhere.com:8934", 22 | databrokerTLSCAFile: "/tmp/tlsca.file", 23 | databrokerTLSCA: caString, 24 | tlsInsecureSkipVerify: "true", 25 | tlsOverrideCertificateName: "override", 26 | namespaces: "one,two,three", 27 | sharedSecret: "secret", 28 | debug: "true", 29 | updateStatusFromService: "some/service", 30 | } { 31 | os.Setenv(envName(k), v) 32 | } 33 | cmd.setupFlags() 34 | assert.Equal(t, []string{"one", "two", "three"}, cmd.Namespaces) 35 | assert.Equal(t, caData, cmd.tlsCA) 36 | assert.Equal(t, true, cmd.debug) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/gen_secrets.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | "github.com/spf13/viper" 9 | corev1 "k8s.io/api/core/v1" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | runtime_ctrl "sigs.k8s.io/controller-runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | 14 | "github.com/pomerium/ingress-controller/util" 15 | ) 16 | 17 | type genSecretsCmd struct { 18 | secrets string 19 | debug bool 20 | 21 | cobra.Command 22 | } 23 | 24 | // GenSecretsCommand generates default secrets 25 | func GenSecretsCommand() (*cobra.Command, error) { 26 | cmd := genSecretsCmd{ 27 | Command: cobra.Command{ 28 | Use: "gen-secrets", 29 | Short: "generates default secrets", 30 | }} 31 | cmd.RunE = cmd.exec 32 | if err := cmd.setupFlags(); err != nil { 33 | return nil, err 34 | } 35 | return &cmd.Command, nil 36 | } 37 | 38 | func (s *genSecretsCmd) setupFlags() error { 39 | flags := s.PersistentFlags() 40 | flags.BoolVar(&s.debug, debug, false, "enable debug logging") 41 | if err := flags.MarkHidden("debug"); err != nil { 42 | return err 43 | } 44 | flags.StringVar(&s.secrets, "secrets", "", "namespaced name of a Secret object to generate") 45 | 46 | v := viper.New() 47 | var err error 48 | flags.VisitAll(func(f *pflag.Flag) { 49 | if err = v.BindEnv(f.Name, envName(f.Name)); err != nil { 50 | return 51 | } 52 | 53 | if !f.Changed && v.IsSet(f.Name) { 54 | val := v.Get(f.Name) 55 | if err = flags.Set(f.Name, fmt.Sprintf("%v", val)); err != nil { 56 | return 57 | } 58 | } 59 | }) 60 | return err 61 | } 62 | 63 | func (s *genSecretsCmd) exec(*cobra.Command, []string) error { 64 | setupLogger(s.debug) 65 | ctx := runtime_ctrl.SetupSignalHandler() 66 | 67 | name, err := util.ParseNamespacedName(s.secrets) 68 | if err != nil { 69 | return fmt.Errorf("%s=%s: %w", globalSettings, s.secrets, err) 70 | } 71 | 72 | cfg, err := runtime_ctrl.GetConfig() 73 | if err != nil { 74 | return fmt.Errorf("get k8s api config: %w", err) 75 | } 76 | 77 | scheme, err := getScheme() 78 | if err != nil { 79 | return fmt.Errorf("scheme: %w", err) 80 | } 81 | 82 | c, err := client.New(cfg, client.Options{Scheme: scheme}) 83 | if err != nil { 84 | return fmt.Errorf("client: %w", err) 85 | } 86 | 87 | // Check if secret already exists 88 | existing := &corev1.Secret{} 89 | err = c.Get(ctx, *name, existing) 90 | if err == nil { 91 | // Secret already exists, exit gracefully 92 | return nil 93 | } 94 | if !apierrors.IsNotFound(err) { 95 | return fmt.Errorf("check existing secret: %w", err) 96 | } 97 | 98 | secret, err := util.NewBootstrapSecrets(*name) 99 | if err != nil { 100 | return fmt.Errorf("generate secrets: %w", err) 101 | } 102 | 103 | if err := c.Create(ctx, secret); err != nil { 104 | return fmt.Errorf("create secret: %w", err) 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /cmd/ingress_opts.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | validate "github.com/go-playground/validator/v10" 7 | "github.com/spf13/pflag" 8 | "k8s.io/apimachinery/pkg/types" 9 | 10 | icsv1 "github.com/pomerium/ingress-controller/apis/ingress/v1" 11 | "github.com/pomerium/ingress-controller/controllers/gateway" 12 | "github.com/pomerium/ingress-controller/controllers/ingress" 13 | "github.com/pomerium/ingress-controller/util" 14 | ) 15 | 16 | type ingressControllerOpts struct { 17 | ClassName string `validate:"required"` 18 | GatewayAPIEnabled bool 19 | GatewayClassName string `validate:"required"` 20 | AnnotationPrefix string `validate:"required"` 21 | Namespaces []string 22 | UpdateStatusFromService string `` 23 | GlobalSettings string `validate:"required"` 24 | } 25 | 26 | const ( 27 | ingressClassControllerName = "name" 28 | experimentalGatewayAPI = "experimental-gateway-api" 29 | gatewayClassControllerName = "gateway-class-controller-name" 30 | annotationPrefix = "prefix" 31 | namespaces = "namespaces" 32 | sharedSecret = "shared-secret" 33 | updateStatusFromService = "update-status-from-service" 34 | globalSettings = "pomerium-config" 35 | ) 36 | 37 | func (s *ingressControllerOpts) setupFlags(flags *pflag.FlagSet) { 38 | flags.StringVar(&s.ClassName, ingressClassControllerName, ingress.DefaultClassControllerName, "IngressClass controller name") 39 | flags.BoolVar(&s.GatewayAPIEnabled, experimentalGatewayAPI, false, "experimental support for the Kubernetes Gateway API") 40 | flags.StringVar(&s.GatewayClassName, gatewayClassControllerName, gateway.DefaultClassControllerName, "GatewayClass controller name") 41 | flags.StringVar(&s.AnnotationPrefix, annotationPrefix, ingress.DefaultAnnotationPrefix, "Ingress annotation prefix") 42 | flags.StringSliceVar(&s.Namespaces, namespaces, nil, "namespaces to watch, or none to watch all namespaces") 43 | flags.StringVar(&s.UpdateStatusFromService, updateStatusFromService, "", "update ingress status from given service status (pomerium-proxy)") 44 | flags.StringVar(&s.GlobalSettings, globalSettings, "", 45 | fmt.Sprintf("namespace/name to a resource of type %s/Settings", icsv1.GroupVersion.Group)) 46 | } 47 | 48 | func (s *ingressControllerOpts) Validate() error { 49 | return validate.New().Struct(s) 50 | } 51 | 52 | func (s *ingressControllerOpts) getGlobalSettings() (*types.NamespacedName, error) { 53 | if s.GlobalSettings == "" { 54 | return nil, nil 55 | } 56 | 57 | name, err := util.ParseNamespacedName(s.GlobalSettings, util.WithClusterScope()) 58 | if err != nil { 59 | return nil, fmt.Errorf("%s=%s: %w", globalSettings, s.GlobalSettings, err) 60 | } 61 | return name, nil 62 | } 63 | 64 | func (s *ingressControllerOpts) getIngressControllerOptions() ([]ingress.Option, error) { 65 | opts := []ingress.Option{ 66 | ingress.WithNamespaces(s.Namespaces), 67 | ingress.WithAnnotationPrefix(s.AnnotationPrefix), 68 | ingress.WithControllerName(s.ClassName), 69 | } 70 | if name, err := s.getGlobalSettings(); err != nil { 71 | return nil, err 72 | } else if name != nil { 73 | opts = append(opts, ingress.WithGlobalSettings(*name)) 74 | } 75 | if s.UpdateStatusFromService != "" { 76 | name, err := util.ParseNamespacedName(s.UpdateStatusFromService) 77 | if err != nil { 78 | return nil, fmt.Errorf("update status from service: %q: %w", s.UpdateStatusFromService, err) 79 | } 80 | opts = append(opts, ingress.WithUpdateIngressStatusFromService(*name)) 81 | } 82 | return opts, nil 83 | } 84 | 85 | func (s *ingressControllerOpts) getGatewayControllerConfig() (*gateway.ControllerConfig, error) { 86 | if !s.GatewayAPIEnabled { 87 | return nil, nil 88 | } 89 | 90 | cfg := &gateway.ControllerConfig{ 91 | ControllerName: s.GatewayClassName, 92 | } 93 | if s.UpdateStatusFromService != "" { 94 | name, err := util.ParseNamespacedName(s.UpdateStatusFromService) 95 | if err != nil { 96 | return cfg, fmt.Errorf("update status from service: %q: %w", s.UpdateStatusFromService, err) 97 | } 98 | cfg.ServiceName = *name 99 | } 100 | return cfg, nil 101 | } 102 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | stress_cmd "github.com/pomerium/ingress-controller/internal/stress/cmd" 9 | ) 10 | 11 | // RootCommand generates default secrets 12 | func RootCommand() (*cobra.Command, error) { 13 | root := cobra.Command{ 14 | Use: "ingress-controller", 15 | Short: "pomerium ingress controller", 16 | SilenceUsage: true, 17 | } 18 | 19 | for name, fn := range map[string]func() (*cobra.Command, error){ 20 | "gen-secrets": GenSecretsCommand, 21 | "controller": ControllerCommand, 22 | "all-in-one": AllInOneCommand, 23 | "stress-test": stress_cmd.Command, 24 | } { 25 | cmd, err := fn() 26 | if err != nil { 27 | return nil, fmt.Errorf("%s: %w", name, err) 28 | } 29 | root.AddCommand(cmd) 30 | } 31 | 32 | return &root, nil 33 | } 34 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | dev 2 | -------------------------------------------------------------------------------- /config/crd/files.go: -------------------------------------------------------------------------------- 1 | // Package crd embeds CRD spec 2 | package crd 3 | 4 | import _ "embed" 5 | 6 | // SettingsCRD is Pomerium CRD Yaml 7 | // 8 | //go:embed bases/ingress.pomerium.io_pomerium.yaml 9 | var SettingsCRD []byte 10 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | resources: 7 | - bases/ingress.pomerium.io_pomerium.yaml 8 | - bases/gateway.pomerium.io_policyfilters.yaml 9 | #+kubebuilder:scaffold:crdkustomizeresource 10 | 11 | # the following config is for teaching kustomize how to do kustomization for CRDs. 12 | configurations: 13 | - kustomizeconfig.yaml 14 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: pomerium 2 | commonLabels: 3 | app.kubernetes.io/name: pomerium 4 | resources: 5 | - ../crd 6 | - ../pomerium 7 | - ../gen_secrets 8 | -------------------------------------------------------------------------------- /config/gateway-api/gatewayclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1 2 | kind: GatewayClass 3 | metadata: 4 | name: pomerium-gateway 5 | spec: 6 | controllerName: "pomerium.io/gateway-controller" 7 | -------------------------------------------------------------------------------- /config/gateway-api/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: pomerium 2 | commonLabels: 3 | app.kubernetes.io/name: pomerium 4 | resources: 5 | - ../default 6 | - gatewayclass.yaml 7 | patches: 8 | - path: role_patch.yaml 9 | target: 10 | group: rbac.authorization.k8s.io 11 | version: v1 12 | kind: ClusterRole 13 | name: pomerium-controller 14 | - patch: |- 15 | - op: add 16 | path: /spec/template/spec/containers/0/args/- 17 | value: '--experimental-gateway-api' 18 | target: 19 | group: apps 20 | version: v1 21 | kind: Deployment 22 | name: pomerium 23 | -------------------------------------------------------------------------------- /config/gateway-api/role_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /rules/- 3 | value: 4 | apiGroups: 5 | - "" 6 | resources: 7 | - namespaces 8 | verbs: 9 | - get 10 | - list 11 | - watch 12 | - op: add 13 | path: /rules/- 14 | value: 15 | apiGroups: 16 | - gateway.networking.k8s.io 17 | resources: 18 | - gatewayclasses 19 | - gateways 20 | - httproutes 21 | - referencegrants 22 | verbs: 23 | - get 24 | - list 25 | - watch 26 | - op: add 27 | path: /rules/- 28 | value: 29 | apiGroups: 30 | - gateway.networking.k8s.io 31 | resources: 32 | - gatewayclasses/status 33 | - gateways/status 34 | - httproutes/status 35 | verbs: 36 | - get 37 | - patch 38 | - update 39 | - op: add 40 | path: /rules/- 41 | value: 42 | apiGroups: 43 | - gateway.pomerium.io 44 | resources: 45 | - policyfilters 46 | verbs: 47 | - get 48 | - list 49 | - watch 50 | - op: add 51 | path: /rules/- 52 | value: 53 | apiGroups: 54 | - gateway.pomerium.io 55 | resources: 56 | - policyfilters/status 57 | verbs: 58 | - get 59 | - patch 60 | - update 61 | -------------------------------------------------------------------------------- /config/gen_secrets/job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: pomerium-gen-secrets 5 | spec: 6 | template: 7 | metadata: 8 | name: pomerium-gen-secrets 9 | spec: 10 | containers: 11 | - name: gen-secrets 12 | args: 13 | - gen-secrets 14 | - --secrets=$(POD_NAMESPACE)/bootstrap 15 | env: 16 | - name: POD_NAMESPACE 17 | valueFrom: 18 | fieldRef: 19 | fieldPath: metadata.namespace 20 | image: pomerium/ingress-controller:main 21 | imagePullPolicy: IfNotPresent 22 | securityContext: 23 | allowPrivilegeEscalation: false 24 | nodeSelector: 25 | kubernetes.io/os: linux 26 | restartPolicy: OnFailure 27 | securityContext: 28 | runAsNonRoot: true 29 | fsGroup: 1000 30 | runAsUser: 1000 31 | serviceAccountName: pomerium-gen-secrets 32 | -------------------------------------------------------------------------------- /config/gen_secrets/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: pomerium 4 | resources: 5 | - job.yaml 6 | - role_binding.yaml 7 | - role.yaml 8 | - service_account.yaml 9 | -------------------------------------------------------------------------------- /config/gen_secrets/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: pomerium-gen-secrets 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - create 13 | - get 14 | -------------------------------------------------------------------------------- /config/gen_secrets/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: pomerium-gen-secrets 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: pomerium-gen-secrets 9 | subjects: 10 | - kind: ServiceAccount 11 | name: pomerium-gen-secrets 12 | -------------------------------------------------------------------------------- /config/gen_secrets/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pomerium-gen-secrets 5 | -------------------------------------------------------------------------------- /config/pomerium/deployment/args.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | args: 11 | - all-in-one 12 | - --pomerium-config=global 13 | - --update-status-from-service=$(POMERIUM_NAMESPACE)/pomerium-proxy 14 | - --metrics-bind-address=$(POD_IP):9090 15 | env: 16 | - name: POMERIUM_NAMESPACE 17 | valueFrom: 18 | fieldRef: 19 | apiVersion: v1 20 | fieldPath: metadata.namespace 21 | - name: POD_IP 22 | valueFrom: 23 | fieldRef: 24 | fieldPath: status.podIP 25 | -------------------------------------------------------------------------------- /config/pomerium/deployment/base.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: pomerium 11 | serviceAccountName: pomerium-controller 12 | terminationGracePeriodSeconds: 10 13 | -------------------------------------------------------------------------------- /config/pomerium/deployment/healthcheck.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | livenessProbe: 11 | httpGet: 12 | path: /healthz 13 | port: 8081 14 | initialDelaySeconds: 15 15 | periodSeconds: 20 16 | readinessProbe: 17 | httpGet: 18 | path: /readyz 19 | port: 8081 20 | initialDelaySeconds: 5 21 | periodSeconds: 10 22 | -------------------------------------------------------------------------------- /config/pomerium/deployment/image.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: pomerium 11 | image: pomerium/ingress-controller:main 12 | imagePullPolicy: Always 13 | -------------------------------------------------------------------------------- /config/pomerium/deployment/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - base.yaml 5 | patches: 6 | - path: args.yaml 7 | - path: image.yaml 8 | - path: ports.yaml 9 | - path: resources.yaml 10 | - path: no-root.yaml 11 | - path: readonly-root-fs.yaml 12 | -------------------------------------------------------------------------------- /config/pomerium/deployment/no-root.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | template: 7 | spec: 8 | securityContext: 9 | runAsNonRoot: true 10 | containers: 11 | - name: pomerium 12 | securityContext: 13 | allowPrivilegeEscalation: false 14 | capabilities: 15 | drop: 16 | - ALL 17 | runAsNonRoot: true 18 | runAsGroup: 65532 19 | runAsUser: 65532 20 | -------------------------------------------------------------------------------- /config/pomerium/deployment/ports.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | ports: 11 | - containerPort: 8443 12 | name: https 13 | protocol: TCP 14 | - containerPort: 443 15 | name: quic 16 | protocol: UDP 17 | - name: http 18 | containerPort: 8080 19 | protocol: TCP 20 | - name: metrics 21 | containerPort: 9090 22 | protocol: TCP 23 | -------------------------------------------------------------------------------- /config/pomerium/deployment/readonly-root-fs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | template: 7 | spec: 8 | nodeSelector: 9 | kubernetes.io/os: linux 10 | containers: 11 | - name: pomerium 12 | securityContext: 13 | readOnlyRootFilesystem: true 14 | env: 15 | - name: TMPDIR 16 | value: "/tmp" 17 | - name: XDG_CACHE_HOME 18 | value: "/tmp" 19 | volumeMounts: 20 | - mountPath: "/tmp" 21 | name: tmp 22 | volumes: 23 | - name: tmp 24 | emptyDir: {} 25 | -------------------------------------------------------------------------------- /config/pomerium/deployment/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | resources: 11 | limits: 12 | cpu: 5000m 13 | memory: 1Gi 14 | requests: 15 | cpu: 300m 16 | memory: 200Mi 17 | -------------------------------------------------------------------------------- /config/pomerium/ingressclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: IngressClass 3 | metadata: 4 | name: pomerium 5 | spec: 6 | controller: pomerium.io/ingress-controller 7 | -------------------------------------------------------------------------------- /config/pomerium/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploys all-in-one controller + core pomerium 3 | # 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | resources: 7 | - namespace.yaml 8 | - ./ingressclass.yaml 9 | - ./deployment 10 | - ./service 11 | - ./rbac 12 | -------------------------------------------------------------------------------- /config/pomerium/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: pomerium 5 | -------------------------------------------------------------------------------- /config/pomerium/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - role.yaml 5 | - role_binding.yaml 6 | - service_account.yaml 7 | -------------------------------------------------------------------------------- /config/pomerium/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: pomerium-controller 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - services 11 | - endpoints 12 | - secrets 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - apiGroups: 18 | - "" 19 | resources: 20 | - services/status 21 | - secrets/status 22 | - endpoints/status 23 | verbs: 24 | - get 25 | - apiGroups: 26 | - networking.k8s.io 27 | resources: 28 | - ingresses 29 | - ingressclasses 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - apiGroups: 35 | - networking.k8s.io 36 | resources: 37 | - ingresses/status 38 | verbs: 39 | - get 40 | - patch 41 | - update 42 | - apiGroups: 43 | - ingress.pomerium.io 44 | resources: 45 | - pomerium 46 | verbs: 47 | - get 48 | - list 49 | - watch 50 | - apiGroups: 51 | - ingress.pomerium.io 52 | resources: 53 | - pomerium/status 54 | verbs: 55 | - get 56 | - update 57 | - patch 58 | - apiGroups: 59 | - "" 60 | resources: 61 | - events 62 | verbs: 63 | - create 64 | - patch 65 | -------------------------------------------------------------------------------- /config/pomerium/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: pomerium-controller 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: pomerium-controller 9 | subjects: 10 | - kind: ServiceAccount 11 | name: pomerium-controller 12 | -------------------------------------------------------------------------------- /config/pomerium/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pomerium-controller 5 | -------------------------------------------------------------------------------- /config/pomerium/service/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - proxy.yaml 5 | - metrics.yaml 6 | -------------------------------------------------------------------------------- /config/pomerium/service/metrics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pomerium-metrics 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - port: 9090 9 | targetPort: metrics 10 | protocol: TCP 11 | name: metrics 12 | -------------------------------------------------------------------------------- /config/pomerium/service/proxy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pomerium-proxy 5 | spec: 6 | type: LoadBalancer 7 | ports: 8 | - port: 443 9 | targetPort: https 10 | protocol: TCP 11 | name: https 12 | - port: 443 13 | targetPort: quic 14 | protocol: UDP 15 | name: quic 16 | - name: http 17 | targetPort: http 18 | protocol: TCP 19 | port: 80 20 | -------------------------------------------------------------------------------- /config/prometheus/coreos-podmonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: PodMonitor 3 | metadata: 4 | name: pomerium 5 | namespace: pomerium 6 | spec: 7 | endpoints: 8 | - path: /metrics 9 | port: metrics 10 | scheme: http 11 | selector: 12 | matchLabels: 13 | app.kubernetes.io/name: pomerium 14 | -------------------------------------------------------------------------------- /config/prometheus/gke-podmonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.googleapis.com/v1 2 | kind: PodMonitoring 3 | metadata: 4 | name: pomerium 5 | spec: 6 | selector: 7 | matchLabels: 8 | app.kubernetes.io/name: pomerium 9 | endpoints: 10 | - port: metrics 11 | path: /metrics 12 | interval: 1m 13 | -------------------------------------------------------------------------------- /config/stress-test/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: stress-test 5 | data: 6 | # how many ingresses to create 7 | ingress-count: "100" 8 | # what is the domain name to use for the ingresses 9 | ingress-domain: "" 10 | # how long to wait for the ingress to be ready. 11 | # this may be proportional to the number of ingresses 12 | # the test would crash and start from scratch if the readiness timeout is not long enough 13 | readiness-timeout: "5m" 14 | -------------------------------------------------------------------------------- /config/stress-test/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stress-test 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | serviceAccountName: pomerium-stress-test 10 | containers: 11 | - name: stress-test 12 | args: 13 | - "stress-test" 14 | image: pomerium/ingress-controller:main 15 | imagePullPolicy: Always 16 | resources: 17 | limits: 18 | memory: "256Mi" 19 | cpu: "500m" 20 | env: 21 | - name: SERVICE_NAME 22 | value: "stress-test-echo" 23 | - name: SERVICE_NAMESPACE 24 | valueFrom: 25 | fieldRef: 26 | apiVersion: v1 27 | fieldPath: metadata.namespace 28 | - name: SERVICE_PORT_NAMES 29 | value: "echo1,echo2" 30 | - name: CONTAINER_PORT_NUMBERS 31 | value: "8081,8082" 32 | - name: INGRESS_CLASS 33 | value: "pomerium" 34 | - name: INGRESS_DOMAIN 35 | valueFrom: 36 | configMapKeyRef: 37 | optional: false 38 | name: stress-test 39 | key: ingress-domain 40 | - name: INGRESS_COUNT 41 | valueFrom: 42 | configMapKeyRef: 43 | optional: false 44 | name: stress-test 45 | key: ingress-count 46 | - name: READINESS_TIMEOUT 47 | valueFrom: 48 | configMapKeyRef: 49 | optional: false 50 | name: stress-test 51 | key: readiness-timeout 52 | ports: 53 | - containerPort: 8081 54 | name: echo1 55 | - containerPort: 8082 56 | name: echo2 57 | -------------------------------------------------------------------------------- /config/stress-test/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: pomerium-stress-test 2 | commonLabels: 3 | app.kubernetes.io/name: pomerium-stress-test 4 | resources: 5 | - namespace.yaml 6 | - ./rbac 7 | - config.yaml 8 | - deployment.yaml 9 | - service.yaml 10 | -------------------------------------------------------------------------------- /config/stress-test/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: pomerium-stress-test 5 | -------------------------------------------------------------------------------- /config/stress-test/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - service_account.yaml 5 | -------------------------------------------------------------------------------- /config/stress-test/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: pomerium-stress-test 6 | rules: 7 | - apiGroups: 8 | - networking.k8s.io 9 | resources: 10 | - ingresses 11 | verbs: 12 | - get 13 | - list 14 | - create 15 | - update 16 | - delete 17 | -------------------------------------------------------------------------------- /config/stress-test/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: pomerium-stress-test 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: pomerium-stress-test 9 | subjects: 10 | - kind: ServiceAccount 11 | name: pomerium-stress-test 12 | -------------------------------------------------------------------------------- /config/stress-test/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pomerium-stress-test 5 | -------------------------------------------------------------------------------- /config/stress-test/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: stress-test-echo 5 | spec: 6 | ports: 7 | - port: 8081 8 | name: echo1 9 | targetPort: echo1 10 | - port: 8082 11 | name: echo2 12 | targetPort: echo2 13 | -------------------------------------------------------------------------------- /controllers/config_controller.go: -------------------------------------------------------------------------------- 1 | // Package controllers contains k8s reconciliation controllers 2 | package controllers 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "sync/atomic" 10 | "time" 11 | 12 | "k8s.io/apimachinery/pkg/types" 13 | runtime_ctrl "sigs.k8s.io/controller-runtime" 14 | "sigs.k8s.io/controller-runtime/pkg/log" 15 | 16 | "github.com/pomerium/pomerium/pkg/grpc/databroker" 17 | 18 | "github.com/pomerium/ingress-controller/controllers/gateway" 19 | "github.com/pomerium/ingress-controller/controllers/ingress" 20 | "github.com/pomerium/ingress-controller/controllers/reporter" 21 | "github.com/pomerium/ingress-controller/controllers/settings" 22 | "github.com/pomerium/ingress-controller/pomerium" 23 | ) 24 | 25 | const ( 26 | leaseDuration = time.Second * 30 27 | ) 28 | 29 | var ( 30 | _ = databroker.LeaserHandler(new(Controller)) 31 | 32 | errWaitingForLease = errors.New("waiting for databroker lease") 33 | ) 34 | 35 | // Controller runs Pomerium configuration reconciliation controllers 36 | // for Ingress and Pomerium Settings CRD objects, if specified 37 | type Controller struct { 38 | pomerium.IngressReconciler 39 | pomerium.GatewayReconciler 40 | pomerium.ConfigReconciler 41 | databroker.DataBrokerServiceClient 42 | MgrOpts runtime_ctrl.Options 43 | // IngressCtrlOpts are the ingress controller options 44 | IngressCtrlOpts []ingress.Option 45 | // GatewayControllerConfig is the Gateway controller config 46 | GatewayControllerConfig *gateway.ControllerConfig 47 | // GlobalSettings if provided, will also reconcile configuration options 48 | GlobalSettings *types.NamespacedName 49 | 50 | running int32 51 | } 52 | 53 | // Run runs controller using lease 54 | func (c *Controller) Run(ctx context.Context) error { 55 | leaser := databroker.NewLeaser("ingress-controller", leaseDuration, c) 56 | return leaser.Run(ctx) 57 | } 58 | 59 | // GetDataBrokerServiceClient implements databroker.LeaseHandler 60 | func (c *Controller) GetDataBrokerServiceClient() databroker.DataBrokerServiceClient { 61 | return c.DataBrokerServiceClient 62 | } 63 | 64 | // RunLeased implements databroker.LeaseHandler 65 | func (c *Controller) RunLeased(ctx context.Context) (err error) { 66 | defer c.setRunning(false) 67 | 68 | cfg, err := runtime_ctrl.GetConfig() 69 | if err != nil { 70 | return fmt.Errorf("get k8s api config: %w", err) 71 | } 72 | mgr, err := runtime_ctrl.NewManager(cfg, c.MgrOpts) 73 | if err != nil { 74 | return fmt.Errorf("unable to create controller manager: %w", err) 75 | } 76 | 77 | if err = ingress.NewIngressController(mgr, c.IngressReconciler, c.getIngressOpts(mgr)...); err != nil { 78 | return fmt.Errorf("create ingress controller: %w", err) 79 | } 80 | if c.GlobalSettings != nil { 81 | if err = settings.NewSettingsController(mgr, c.ConfigReconciler, *c.GlobalSettings, "pomerium-crd", true); err != nil { 82 | return fmt.Errorf("create settings controller: %w", err) 83 | } 84 | } else { 85 | log.FromContext(ctx).V(1).Info("no Pomerium CRD") 86 | } 87 | 88 | if c.GatewayControllerConfig != nil { 89 | err := gateway.NewControllers(ctx, mgr, c.GatewayReconciler, *c.GatewayControllerConfig) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | c.setRunning(true) 96 | if err = mgr.Start(ctx); err != nil { 97 | return fmt.Errorf("running controller: %w", err) 98 | } 99 | return nil 100 | } 101 | 102 | func (c *Controller) setRunning(running bool) { 103 | if running { 104 | atomic.StoreInt32(&c.running, 1) 105 | } else { 106 | atomic.StoreInt32(&c.running, 0) 107 | } 108 | } 109 | 110 | // ReadyzCheck reports whether controller is ready 111 | func (c *Controller) ReadyzCheck(_ *http.Request) error { 112 | val := atomic.LoadInt32(&c.running) 113 | if val == 0 { 114 | return errWaitingForLease 115 | } 116 | return nil 117 | } 118 | 119 | func (c *Controller) getIngressOpts(mgr runtime_ctrl.Manager) []ingress.Option { 120 | if c.GlobalSettings == nil { 121 | return c.IngressCtrlOpts 122 | } 123 | 124 | rep := reporter.SettingsReporter{ 125 | NamespacedName: *c.GlobalSettings, 126 | Client: mgr.GetClient(), 127 | } 128 | 129 | return append(c.IngressCtrlOpts, ingress.WithIngressStatusReporter( 130 | &reporter.IngressSettingsReporter{ 131 | SettingsReporter: rep, 132 | }, 133 | &reporter.IngressSettingsEventReporter{ 134 | EventRecorder: mgr.GetEventRecorderFor("pomerium-ingress"), 135 | SettingsReporter: rep, 136 | })) 137 | } 138 | -------------------------------------------------------------------------------- /controllers/deps/deps.go: -------------------------------------------------------------------------------- 1 | // Package deps implements dependencies management 2 | package deps 3 | 4 | import ( 5 | "context" 6 | 7 | "k8s.io/apimachinery/pkg/types" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | "sigs.k8s.io/controller-runtime/pkg/handler" 10 | "sigs.k8s.io/controller-runtime/pkg/log" 11 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 12 | 13 | "github.com/pomerium/ingress-controller/model" 14 | ) 15 | 16 | // GetDependantMapFunc produces list of dependencies for reconciliation of a given kind 17 | func GetDependantMapFunc(r model.Registry, kind string) handler.MapFunc { 18 | return func(ctx context.Context, obj client.Object) []reconcile.Request { 19 | key := model.Key{ 20 | Kind: kind, 21 | NamespacedName: types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, 22 | } 23 | deps := r.Deps(key) 24 | reqs := make([]reconcile.Request, 0, len(deps)) 25 | for _, k := range deps { 26 | reqs = append(reqs, reconcile.Request{NamespacedName: k.NamespacedName}) 27 | } 28 | log.FromContext(ctx).V(1).Info("watch deps", "src", key, "dst", reqs, "deps", r.Deps(key)) 29 | return reqs 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /controllers/deps/registry_client.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 9 | "sigs.k8s.io/controller-runtime/pkg/log" 10 | 11 | "github.com/pomerium/ingress-controller/model" 12 | ) 13 | 14 | type trackingClient struct { 15 | client.Client 16 | model.Registry 17 | model.Key 18 | } 19 | 20 | // NewClient creates a client that watches Get requests 21 | // and marks these objects as dependencies in the registry, including those that were not currently found 22 | func NewClient(c client.Client, r model.Registry, k model.Key) client.Client { 23 | return &trackingClient{c, r, k} 24 | } 25 | 26 | // Get retrieves an obj for the given object key from the Kubernetes Cluster. 27 | func (c *trackingClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 28 | dep, err := c.makeKey(key, obj) 29 | if err != nil { 30 | return fmt.Errorf("dependency key: %w", err) 31 | } 32 | 33 | c.Registry.Add(c.Key, *dep) 34 | 35 | err = c.Client.Get(ctx, key, obj, opts...) 36 | log.FromContext(ctx).V(1).Info("Get", "key", *dep, "err", err) 37 | return err 38 | } 39 | 40 | func (c *trackingClient) makeKey(name client.ObjectKey, obj client.Object) (*model.Key, error) { 41 | gvk, err := apiutil.GVKForObject(obj, c.Scheme()) 42 | if err != nil { 43 | return nil, fmt.Errorf("GVK was not registered for %s/%s", name, obj.GetObjectKind()) 44 | } 45 | kind := gvk.Kind 46 | if kind == "" { 47 | return nil, fmt.Errorf("no Kind available for object %s", name) 48 | } 49 | return &model.Key{Kind: kind, NamespacedName: name}, nil 50 | } 51 | -------------------------------------------------------------------------------- /controllers/gateway/conditions.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | func upsertConditions( 6 | conditions *[]metav1.Condition, 7 | observedGeneration int64, 8 | condition ...metav1.Condition, 9 | ) (modified bool) { 10 | for _, c := range condition { 11 | if upsertCondition(conditions, observedGeneration, c) { 12 | modified = true 13 | } 14 | } 15 | return modified 16 | } 17 | 18 | func upsertCondition( 19 | conditions *[]metav1.Condition, 20 | observedGeneration int64, 21 | condition metav1.Condition, 22 | ) (modified bool) { 23 | condition.ObservedGeneration = observedGeneration 24 | condition.LastTransitionTime = metav1.Now() 25 | 26 | conds := *conditions 27 | for i := range conds { 28 | if conds[i].Type == condition.Type { 29 | // Existing condition found. 30 | if conds[i].ObservedGeneration == condition.ObservedGeneration && 31 | conds[i].Status == condition.Status && 32 | conds[i].Reason == condition.Reason && 33 | conds[i].Message == condition.Message { 34 | return false 35 | } 36 | conds[i] = condition 37 | return true 38 | } 39 | } 40 | // No existing condition found, so add it. 41 | *conditions = append(*conditions, condition) 42 | return true 43 | } 44 | -------------------------------------------------------------------------------- /controllers/gateway/controller.go: -------------------------------------------------------------------------------- 1 | // Package gateway contains controllers for Gateway API objects. 2 | package gateway 3 | 4 | import ( 5 | context "context" 6 | "fmt" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | ctrl "sigs.k8s.io/controller-runtime" 11 | "sigs.k8s.io/controller-runtime/pkg/builder" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/handler" 14 | "sigs.k8s.io/controller-runtime/pkg/predicate" 15 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 16 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 17 | gateway_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 18 | 19 | icgv1alpha1 "github.com/pomerium/ingress-controller/apis/gateway/v1alpha1" 20 | "github.com/pomerium/ingress-controller/pomerium" 21 | ) 22 | 23 | // DefaultClassControllerName is the default GatewayClass ControllerName. 24 | const DefaultClassControllerName = "pomerium.io/gateway-controller" 25 | 26 | // ControllerConfig contains configuration options for the Gateway controller. 27 | type ControllerConfig struct { 28 | // ControllerName associates this controller with a GatewayClass. 29 | ControllerName string 30 | // Gateway addresses are determined from this service. 31 | ServiceName types.NamespacedName 32 | } 33 | 34 | // NewControllers sets up GatewayClass and Gateway controllers. 35 | func NewControllers( 36 | ctx context.Context, 37 | mgr ctrl.Manager, 38 | pgr pomerium.GatewayReconciler, 39 | config ControllerConfig, 40 | ) error { 41 | if err := NewGatewayClassController(mgr, config.ControllerName); err != nil { 42 | return fmt.Errorf("couldn't create GatewayClass controller: %w", err) 43 | } 44 | if err := NewGatewayController(ctx, mgr, pgr, config); err != nil { 45 | return fmt.Errorf("couldn't create Gateway controller: %w", err) 46 | } 47 | return nil 48 | } 49 | 50 | type gatewayController struct { 51 | client.Client 52 | pomerium.GatewayReconciler 53 | ControllerConfig 54 | 55 | extensionFilters map[refKey]objectAndFilter 56 | } 57 | 58 | // NewGatewayController creates and registers a new controller for Gateway objects. 59 | func NewGatewayController( 60 | ctx context.Context, 61 | mgr ctrl.Manager, 62 | pgr pomerium.GatewayReconciler, 63 | config ControllerConfig, 64 | ) error { 65 | gtc := &gatewayController{ 66 | Client: mgr.GetClient(), 67 | GatewayReconciler: pgr, 68 | ControllerConfig: config, 69 | extensionFilters: make(map[refKey]objectAndFilter), 70 | } 71 | 72 | err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.Secret{}, "type", 73 | func(o client.Object) []string { return []string{string(o.(*corev1.Secret).Type)} }) 74 | if err != nil { 75 | return fmt.Errorf("couldn't create index on Secret type: %w", err) 76 | } 77 | 78 | // All updates will trigger the same reconcile request. 79 | enqueueRequest := handler.EnqueueRequestsFromMapFunc( 80 | func(_ context.Context, _ client.Object) []reconcile.Request { 81 | return []reconcile.Request{{ 82 | NamespacedName: types.NamespacedName{ 83 | Name: config.ControllerName, 84 | }, 85 | }} 86 | }) 87 | 88 | err = ctrl.NewControllerManagedBy(mgr). 89 | Named("gateway"). 90 | Watches( 91 | &gateway_v1.Gateway{}, 92 | enqueueRequest, 93 | builder.WithPredicates(predicate.GenerationChangedPredicate{}), 94 | ). 95 | Watches( 96 | &gateway_v1.HTTPRoute{}, 97 | enqueueRequest, 98 | builder.WithPredicates(predicate.GenerationChangedPredicate{}), 99 | ). 100 | Watches(&corev1.Secret{}, enqueueRequest). 101 | Watches(&corev1.Namespace{}, enqueueRequest). 102 | Watches(&corev1.Service{}, enqueueRequest). 103 | Watches(&gateway_v1beta1.ReferenceGrant{}, enqueueRequest). 104 | Watches(&icgv1alpha1.PolicyFilter{}, enqueueRequest). 105 | Complete(gtc) 106 | if err != nil { 107 | return fmt.Errorf("build controller: %w", err) 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func (c *gatewayController) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { 114 | o, err := c.fetchObjects(ctx) 115 | if err != nil { 116 | return ctrl.Result{}, err 117 | } 118 | 119 | config, err := c.processGateways(ctx, o) 120 | if err != nil { 121 | return ctrl.Result{}, err 122 | } 123 | 124 | _, err = c.SetGatewayConfig(ctx, config) 125 | if err != nil { 126 | return ctrl.Result{}, err 127 | } 128 | 129 | return ctrl.Result{}, nil 130 | } 131 | -------------------------------------------------------------------------------- /controllers/gateway/extensionfilters.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | context "context" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | 10 | icgv1alpha1 "github.com/pomerium/ingress-controller/apis/gateway/v1alpha1" 11 | "github.com/pomerium/ingress-controller/model" 12 | "github.com/pomerium/ingress-controller/pomerium/gateway" 13 | ) 14 | 15 | func (c *gatewayController) processExtensionFilters( 16 | ctx context.Context, 17 | config *model.GatewayConfig, 18 | o *objects, 19 | ) error { 20 | for _, pf := range o.PolicyFilters { 21 | if err := c.processPolicyFilter(ctx, pf); err != nil { 22 | return err 23 | } 24 | } 25 | config.ExtensionFilters = makeExtensionFilterMap(c.extensionFilters) 26 | return nil 27 | } 28 | 29 | func (c *gatewayController) processPolicyFilter( 30 | ctx context.Context, 31 | pf *icgv1alpha1.PolicyFilter, 32 | ) error { 33 | // Check to see if we already have a parsed representation of this filter. 34 | k := refKeyForObject(pf) 35 | f := c.extensionFilters[k] 36 | if f.object != nil && f.object.GetGeneration() == pf.Generation { 37 | return nil 38 | } 39 | 40 | filter, err := gateway.NewPolicyFilter(pf) 41 | 42 | // Set a "Valid" condition with information about whether the policy could be parsed. 43 | validCondition := metav1.Condition{ 44 | Type: "Valid", 45 | } 46 | if err == nil { 47 | validCondition.Status = metav1.ConditionTrue 48 | validCondition.Reason = "Valid" 49 | } else { 50 | validCondition.Status = metav1.ConditionFalse 51 | validCondition.Reason = "Invalid" 52 | validCondition.Message = err.Error() 53 | } 54 | if upsertCondition(&pf.Status.Conditions, pf.Generation, validCondition) { 55 | if err := c.Status().Update(ctx, pf); err != nil { 56 | return fmt.Errorf("couldn't update status for PolicyFilter %q: %w", pf.Name, err) 57 | } 58 | } 59 | 60 | c.extensionFilters[k] = objectAndFilter{pf, filter} 61 | 62 | return nil 63 | } 64 | 65 | type objectAndFilter struct { 66 | object client.Object 67 | filter model.ExtensionFilter 68 | } 69 | 70 | func makeExtensionFilterMap( 71 | extensionFilters map[refKey]objectAndFilter, 72 | ) map[model.ExtensionFilterKey]model.ExtensionFilter { 73 | m := make(map[model.ExtensionFilterKey]model.ExtensionFilter) 74 | for k, f := range extensionFilters { 75 | key := model.ExtensionFilterKey{Kind: k.Kind, Namespace: k.Namespace, Name: k.Name} 76 | m[key] = f.filter 77 | } 78 | return m 79 | } 80 | -------------------------------------------------------------------------------- /controllers/gateway/gatewayclass.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | context "context" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ctrl "sigs.k8s.io/controller-runtime" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 10 | ) 11 | 12 | type gatewayClassController struct { 13 | client.Client 14 | controllerName string 15 | } 16 | 17 | // NewGatewayClassController creates and registers a new controller for GatewayClass objects. 18 | // This controller does just one thing: it sets the "Accepted" status condition. 19 | func NewGatewayClassController( 20 | mgr ctrl.Manager, 21 | controllerName string, 22 | ) error { 23 | gtcc := &gatewayClassController{ 24 | Client: mgr.GetClient(), 25 | controllerName: controllerName, 26 | } 27 | 28 | return ctrl.NewControllerManagedBy(mgr). 29 | Named("gateway-class"). 30 | For(&gateway_v1.GatewayClass{}). 31 | Complete(gtcc) 32 | } 33 | 34 | func (c *gatewayClassController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 35 | var gc gateway_v1.GatewayClass 36 | if err := c.Get(ctx, req.NamespacedName, &gc); err != nil { 37 | return ctrl.Result{}, err 38 | } 39 | 40 | if gc.Spec.ControllerName != gateway_v1.GatewayController(c.controllerName) { 41 | return ctrl.Result{}, nil 42 | } 43 | 44 | if setGatewayClassAccepted(&gc) { 45 | // Condition changed, need to update status. 46 | if err := c.Status().Update(ctx, &gc); err != nil { 47 | return ctrl.Result{}, err 48 | } 49 | } 50 | 51 | return ctrl.Result{}, nil 52 | } 53 | 54 | func setGatewayClassAccepted(gc *gateway_v1.GatewayClass) (modified bool) { 55 | return upsertCondition(&gc.Status.Conditions, gc.Generation, metav1.Condition{ 56 | Type: string(gateway_v1.GatewayClassConditionStatusAccepted), 57 | Status: metav1.ConditionTrue, 58 | Reason: string(gateway_v1.GatewayClassReasonAccepted), 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /controllers/gateway/referencegrant.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "github.com/hashicorp/go-set/v3" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | gateway_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 7 | ) 8 | 9 | // referenceGrantMap is a map representation of all ReferenceGrants. Keys represent a target object 10 | // (corresponding to a ReferenceGrantTo) and the values represent source objects (corresponding to 11 | // a ReferenceGrantFrom). There are a few subtleties: 12 | // - A refKey with an empty Name represents any object of that kind within the namespace. 13 | // - A refKey used as a key in this map may or may not have an empty Name, as a ReferenceGrantTo 14 | // contains an optional Name field. 15 | // - A refKey in one of the value collections should always have an empty Name, as a 16 | // ReferenceGrantFrom cannot reference a specific object by name. 17 | type referenceGrantMap map[refKey]set.Collection[refKey] 18 | 19 | func buildReferenceGrantMap(grants []gateway_v1beta1.ReferenceGrant) referenceGrantMap { 20 | m := referenceGrantMap{} 21 | for i := range grants { 22 | g := &grants[i] 23 | sourceSet := set.FromFunc(g.Spec.From, refKeyForReferenceGrantFrom) 24 | for _, to := range g.Spec.To { 25 | k := refKeyForReferenceGrantTo(g.Namespace, to) 26 | m[k] = safeUnion(m[k], sourceSet) 27 | } 28 | } 29 | return m 30 | } 31 | 32 | func (m referenceGrantMap) allowed(from client.Object, toKey refKey) bool { 33 | // A ReferenceGrant is not required for references within a single namespace. 34 | if from.GetNamespace() == toKey.Namespace { 35 | return true 36 | } 37 | 38 | fromKey := refKeyForObject(from) 39 | fromKey.Name = "" 40 | 41 | if s := m[toKey]; s != nil && s.Contains(fromKey) { 42 | return true // specific "To" object is allowed 43 | } 44 | toKey.Name = "" 45 | if s := m[toKey]; s != nil && s.Contains(fromKey) { 46 | return true // entire "To" namespace is allowed 47 | } 48 | return false 49 | } 50 | 51 | func safeUnion[T comparable](a, b set.Collection[T]) set.Collection[T] { 52 | if a == nil { 53 | return b 54 | } else if b == nil { 55 | return a 56 | } 57 | return a.Union(b) 58 | } 59 | -------------------------------------------------------------------------------- /controllers/gateway/refkey.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 7 | gateway_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 8 | ) 9 | 10 | // refKey is an object reference in a form suitable for use as a map key. 11 | // Gateway references have some optional fields with default values that vary by type. 12 | // In a refKey these defaults should be made explicit. 13 | type refKey struct { 14 | Group string 15 | Kind string 16 | Namespace string 17 | Name string 18 | } 19 | 20 | func refKeyForObject(obj client.Object) refKey { 21 | gvk := obj.GetObjectKind().GroupVersionKind() 22 | return refKey{ 23 | Group: gvk.Group, 24 | Kind: gvk.Kind, 25 | Namespace: obj.GetNamespace(), 26 | Name: obj.GetName(), 27 | } 28 | } 29 | 30 | func refKeyForParentRef(obj client.Object, ref *gateway_v1.ParentReference) refKey { 31 | // See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.ParentReference 32 | // "When unspecified, “gateway.networking.k8s.io” is inferred." 33 | group := gateway_v1.GroupName 34 | if ref.Group != nil { 35 | group = string(*ref.Group) 36 | } 37 | // Kind appears to have a default value but I don't see this clearly spelled out in the API 38 | // reference. I think Gateway is the only kind we care about in practice. 39 | kind := "Gateway" 40 | if ref.Kind != nil { 41 | kind = string(*ref.Kind) 42 | } 43 | namespace := obj.GetNamespace() 44 | if ref.Namespace != nil { 45 | namespace = string(*ref.Namespace) 46 | } 47 | return refKey{ 48 | Group: group, 49 | Kind: kind, 50 | Namespace: namespace, 51 | Name: string(ref.Name), 52 | } 53 | } 54 | 55 | func refKeyForCertificateRef(obj client.Object, ref *gateway_v1.SecretObjectReference) refKey { 56 | // See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.SecretObjectReference 57 | // "When unspecified or empty string, core API group is inferred." 58 | group := corev1.GroupName 59 | if ref.Group != nil { 60 | group = string(*ref.Group) 61 | } 62 | // "SecretObjectReference identifies an API object including its namespace, defaulting to Secret." 63 | kind := "Secret" 64 | if ref.Kind != nil { 65 | kind = string(*ref.Kind) 66 | } 67 | namespace := obj.GetNamespace() 68 | if ref.Namespace != nil { 69 | namespace = string(*ref.Namespace) 70 | } 71 | return refKey{ 72 | Group: group, 73 | Kind: kind, 74 | Namespace: namespace, 75 | Name: string(ref.Name), 76 | } 77 | } 78 | 79 | func refKeyForBackendRef(obj client.Object, ref *gateway_v1.BackendObjectReference) refKey { 80 | // See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.BackendObjectReference 81 | // "When unspecified or empty string, core API group is inferred." 82 | group := corev1.GroupName 83 | if ref.Group != nil { 84 | group = string(*ref.Group) 85 | } 86 | // "Defaults to "Service" when not specified." 87 | kind := "Service" 88 | if ref.Kind != nil { 89 | kind = string(*ref.Kind) 90 | } 91 | namespace := obj.GetNamespace() 92 | if ref.Namespace != nil { 93 | namespace = string(*ref.Namespace) 94 | } 95 | return refKey{ 96 | Group: group, 97 | Kind: kind, 98 | Namespace: namespace, 99 | Name: string(ref.Name), 100 | } 101 | } 102 | 103 | func refKeyForReferenceGrantFrom(from gateway_v1beta1.ReferenceGrantFrom) refKey { 104 | return refKey{ 105 | Group: string(from.Group), 106 | Kind: string(from.Kind), 107 | Namespace: string(from.Namespace), 108 | } 109 | } 110 | 111 | func refKeyForReferenceGrantTo(namespace string, to gateway_v1beta1.ReferenceGrantTo) refKey { 112 | var name string 113 | if to.Name != nil { 114 | name = string(*to.Name) 115 | } 116 | return refKey{ 117 | Group: string(to.Group), 118 | Kind: string(to.Kind), 119 | Namespace: namespace, 120 | Name: name, 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /controllers/ingress/builder.go: -------------------------------------------------------------------------------- 1 | // Package ingress implements Ingress controller functions 2 | package ingress 3 | 4 | import ( 5 | "fmt" 6 | 7 | ctrl "sigs.k8s.io/controller-runtime" 8 | 9 | "github.com/pomerium/ingress-controller/controllers/reporter" 10 | "github.com/pomerium/ingress-controller/model" 11 | "github.com/pomerium/ingress-controller/pomerium" 12 | ) 13 | 14 | const ( 15 | // DefaultAnnotationPrefix defines prefix that would be watched for Ingress annotations 16 | DefaultAnnotationPrefix = "ingress.pomerium.io" 17 | // DefaultClassControllerName is controller name 18 | DefaultClassControllerName = "pomerium.io/ingress-controller" 19 | ) 20 | 21 | // NewIngressController creates new controller runtime 22 | func NewIngressController( 23 | mgr ctrl.Manager, 24 | pcr pomerium.IngressReconciler, 25 | opts ...Option, 26 | ) error { 27 | registry := model.NewRegistry() 28 | ic := &ingressController{ 29 | annotationPrefix: DefaultAnnotationPrefix, 30 | controllerName: DefaultClassControllerName, 31 | IngressReconciler: pcr, 32 | Client: mgr.GetClient(), 33 | Registry: registry, 34 | MultiIngressStatusReporter: []reporter.IngressStatusReporter{ 35 | &reporter.IngressEventReporter{EventRecorder: mgr.GetEventRecorderFor(controllerName)}, 36 | &reporter.IngressLogReporter{V: 1, Name: controllerName}, 37 | }, 38 | } 39 | ic.initComplete = newOnce(ic.reconcileInitial) 40 | for _, opt := range opts { 41 | opt(ic) 42 | } 43 | 44 | if err := ic.SetupWithManager(mgr); err != nil { 45 | return fmt.Errorf("unable to create controller: %w", err) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func arrayToMap(in []string) map[string]bool { 52 | out := make(map[string]bool, len(in)) 53 | for _, k := range in { 54 | out[k] = true 55 | } 56 | return out 57 | } 58 | -------------------------------------------------------------------------------- /controllers/ingress/controller_test.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.uber.org/mock/gomock" 9 | networkingv1 "k8s.io/api/networking/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | 13 | controllers_mock "github.com/pomerium/ingress-controller/controllers/mock" 14 | ) 15 | 16 | func TestManagingIngressClass(t *testing.T) { 17 | pomeriumControllerName := "pomerium.io/ingress-controller" 18 | pomeriumIngressClass := "pomerium" 19 | otherIngressClass := "legacy" 20 | otherControllerName := "legacy.com/ingress" 21 | 22 | ctx := context.Background() 23 | mc := controllers_mock.NewMockClient(gomock.NewController(t)) 24 | ctrl := ingressController{ 25 | controllerName: pomeriumControllerName, 26 | annotationPrefix: DefaultAnnotationPrefix, 27 | Client: mc, 28 | endpointsKind: "Endpoints", 29 | ingressKind: "Ingress", 30 | ingressClassKind: "IngressClass", 31 | secretKind: "Secret", 32 | serviceKind: "Service", 33 | initComplete: newOnce(func(_ context.Context) error { return nil }), 34 | } 35 | 36 | testCases := []struct { 37 | title string 38 | ingress networkingv1.Ingress 39 | classes []networkingv1.IngressClass 40 | result bool 41 | }{ 42 | { 43 | "our ingress class", 44 | networkingv1.Ingress{ 45 | Spec: networkingv1.IngressSpec{ 46 | IngressClassName: &pomeriumIngressClass, 47 | }, 48 | }, 49 | []networkingv1.IngressClass{{ 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: pomeriumIngressClass, 52 | }, 53 | Spec: networkingv1.IngressClassSpec{ 54 | Controller: pomeriumControllerName, 55 | }, 56 | }}, 57 | true, 58 | }, 59 | { 60 | "ignore other ingress classes", 61 | networkingv1.Ingress{ 62 | Spec: networkingv1.IngressSpec{ 63 | IngressClassName: &otherIngressClass, 64 | }, 65 | }, 66 | []networkingv1.IngressClass{{ 67 | ObjectMeta: metav1.ObjectMeta{ 68 | Name: otherIngressClass, 69 | }, 70 | Spec: networkingv1.IngressClassSpec{ 71 | Controller: otherControllerName, 72 | }, 73 | }}, 74 | false, 75 | }, 76 | { 77 | "deprecated method used by HTTP solvers", 78 | networkingv1.Ingress{ 79 | ObjectMeta: metav1.ObjectMeta{ 80 | Annotations: map[string]string{ 81 | IngressClassAnnotationKey: pomeriumIngressClass, 82 | }, 83 | }, 84 | }, 85 | []networkingv1.IngressClass{{ 86 | ObjectMeta: metav1.ObjectMeta{ 87 | Name: pomeriumIngressClass, 88 | }, 89 | Spec: networkingv1.IngressClassSpec{ 90 | Controller: pomeriumControllerName, 91 | }, 92 | }}, 93 | true, 94 | }, 95 | { 96 | "default ingress", 97 | networkingv1.Ingress{}, 98 | []networkingv1.IngressClass{{ 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Name: pomeriumIngressClass, 101 | Annotations: map[string]string{ 102 | IngressClassDefaultAnnotationKey: "true", 103 | }, 104 | }, 105 | Spec: networkingv1.IngressClassSpec{ 106 | Controller: pomeriumControllerName, 107 | }, 108 | }}, 109 | true, 110 | }, 111 | } 112 | 113 | var classes []networkingv1.IngressClass 114 | mc.EXPECT().List(ctx, gomock.AssignableToTypeOf(&networkingv1.IngressClassList{})). 115 | Do(func(_ context.Context, dst *networkingv1.IngressClassList, _ ...client.ListOption) { 116 | dst.Items = classes 117 | }). 118 | Return(nil). 119 | Times(len(testCases)) 120 | for _, tc := range testCases { 121 | classes = tc.classes 122 | res, err := ctrl.isManaging(ctx, &tc.ingress) 123 | if assert.NoError(t, err, tc.title) { 124 | if assert.Equal(t, tc.result, res.managed, tc.title) && !tc.result { 125 | assert.NotEmpty(t, res.reasonIfNot, "if not managing, reason must be provided") 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /controllers/ingress/deps.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | networkingv1 "k8s.io/api/networking/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/handler" 12 | "sigs.k8s.io/controller-runtime/pkg/log" 13 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 14 | 15 | "github.com/pomerium/ingress-controller/model" 16 | ) 17 | 18 | // getDependantIngressFn returns for a given object kind (i.e. a secret) a function 19 | // that would return ingress objects keys that depend from this object 20 | func (r *ingressController) getDependantIngressFn(kind string) handler.MapFunc { 21 | return func(ctx context.Context, a client.Object) []reconcile.Request { 22 | if !r.isWatching(a) { 23 | return nil 24 | } 25 | 26 | name := types.NamespacedName{Name: a.GetName(), Namespace: a.GetNamespace()} 27 | deps := r.DepsOfKind(model.Key{Kind: kind, NamespacedName: name}, r.ingressKind) 28 | reqs := make([]reconcile.Request, 0, len(deps)) 29 | for _, k := range deps { 30 | reqs = append(reqs, reconcile.Request{NamespacedName: k.NamespacedName}) 31 | } 32 | log.FromContext(ctx). 33 | WithValues("kind", kind).V(5). 34 | Info("watch", "name", fmt.Sprintf("%s/%s", a.GetNamespace(), a.GetName()), "deps", reqs) 35 | return reqs 36 | } 37 | } 38 | 39 | func (r *ingressController) watchIngressClass() handler.MapFunc { 40 | return func(ctx context.Context, a client.Object) []reconcile.Request { 41 | logger := log.FromContext(ctx) 42 | ctx, cancel := context.WithTimeout(ctx, initialReconciliationTimeout) 43 | defer cancel() 44 | 45 | _ = r.initComplete.yield(ctx) 46 | 47 | ic, ok := a.(*networkingv1.IngressClass) 48 | if !ok { 49 | logger.Error(fmt.Errorf("got %s", reflect.TypeOf(a)), "expected IngressClass") 50 | return nil 51 | } 52 | if ic.Spec.Controller != r.controllerName { 53 | return nil 54 | } 55 | il := new(networkingv1.IngressList) 56 | err := r.Client.List(ctx, il) 57 | if err != nil { 58 | logger.Error(err, "list") 59 | return nil 60 | } 61 | deps := make([]reconcile.Request, 0, len(il.Items)) 62 | for i := range il.Items { 63 | deps = append(deps, reconcile.Request{ 64 | NamespacedName: types.NamespacedName{ 65 | Name: il.Items[i].Name, 66 | Namespace: il.Items[i].Namespace, 67 | }, 68 | }) 69 | } 70 | logger.V(5).Info("watch", "deps", deps, "ingressClass", a.GetName()) 71 | return deps 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /controllers/ingress/ingress_class.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | 9 | networkingv1 "k8s.io/api/networking/v1" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | "sigs.k8s.io/controller-runtime/pkg/log" 12 | ) 13 | 14 | const ( 15 | // IngressClassAnnotationKey although deprecated, still may be used by the HTTP solvers even for v1 Ingress resources 16 | // see https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#deprecating-the-ingress-class-annotation 17 | IngressClassAnnotationKey = "kubernetes.io/ingress.class" 18 | // IngressClassDefaultAnnotationKey see https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class 19 | IngressClassDefaultAnnotationKey = "ingressclass.kubernetes.io/is-default-class" 20 | ) 21 | 22 | type ingressManageResult struct { 23 | reasonIfNot string 24 | managed bool 25 | } 26 | 27 | var ( 28 | ingressIsManaged = &ingressManageResult{managed: true} 29 | ) 30 | 31 | func (r *ingressController) isManaging(ctx context.Context, ing *networkingv1.Ingress) (*ingressManageResult, error) { 32 | _, err := r.getManagingClass(ctx, ing) 33 | if err == nil { 34 | return ingressIsManaged, nil 35 | } 36 | 37 | if status := apierrors.APIStatus(nil); errors.As(err, &status) { 38 | return nil, err 39 | } 40 | 41 | return &ingressManageResult{ 42 | managed: false, 43 | reasonIfNot: err.Error(), 44 | }, nil 45 | } 46 | 47 | func (r *ingressController) getManagingClass(ctx context.Context, ing *networkingv1.Ingress) (*networkingv1.IngressClass, error) { 48 | // if controller is started with explicit list of namespaces to watch, 49 | // ignore all ingress resources coming from other namespaces 50 | if len(r.namespaces) > 0 && !r.namespaces[ing.Namespace] { 51 | return nil, fmt.Errorf("ingress %s/%s is not in the namespace list this controller is managing", ing.Namespace, ing.Name) 52 | } 53 | 54 | icl := new(networkingv1.IngressClassList) 55 | if err := r.Client.List(ctx, icl); err != nil { 56 | return nil, err 57 | } 58 | 59 | var className string 60 | if ing.Spec.IngressClassName != nil { 61 | className = *ing.Spec.IngressClassName 62 | } else if className = ing.Annotations[IngressClassAnnotationKey]; className != "" { 63 | log.FromContext(ctx).Info(fmt.Sprintf("use of deprecated annotation %s, please use spec.ingressClassName instead", IngressClassAnnotationKey)) 64 | } 65 | 66 | if className == "" { 67 | for _, ic := range icl.Items { 68 | if ic.Spec.Controller != r.controllerName { 69 | continue 70 | } 71 | class := ic 72 | if isDefault, _ := isDefaultIngressClass(&class); isDefault { 73 | return &ic, nil 74 | } 75 | } 76 | return nil, fmt.Errorf("the ingress did not specify an ingressClass, and no ingressClass managed by controller %s is marked as default", r.controllerName) 77 | } 78 | 79 | for _, ic := range icl.Items { 80 | if ic.Spec.Controller != r.controllerName { 81 | continue 82 | } 83 | if className == ic.Name { 84 | return &ic, nil 85 | } 86 | } 87 | 88 | return nil, fmt.Errorf("IngressClass %s not found or is not assigned to this controller %s", className, r.controllerName) 89 | } 90 | 91 | func getAnnotation(dict map[string]string, key string) (string, error) { 92 | if dict == nil { 93 | return "", fmt.Errorf("annotation %s is missing", key) 94 | } 95 | txt, ok := dict[key] 96 | if !ok { 97 | return "", fmt.Errorf("annotation %s is missing", key) 98 | } 99 | return txt, nil 100 | } 101 | 102 | func isDefaultIngressClass(ic *networkingv1.IngressClass) (bool, error) { 103 | txt, err := getAnnotation(ic.Annotations, IngressClassDefaultAnnotationKey) 104 | if err != nil { 105 | return false, err 106 | } 107 | val, err := strconv.ParseBool(txt) 108 | if err != nil { 109 | return false, fmt.Errorf("invalid value for annotation %s: %w", IngressClassDefaultAnnotationKey, err) 110 | } 111 | return val, nil 112 | } 113 | -------------------------------------------------------------------------------- /controllers/ingress/once.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type once struct { 8 | execCtx chan context.Context 9 | result chan error 10 | } 11 | 12 | func newOnce(runnable func(ctx context.Context) error) *once { 13 | o := &once{ 14 | execCtx: make(chan context.Context), 15 | result: make(chan error), 16 | } 17 | go func() { 18 | ctx := <-o.execCtx 19 | err := runnable(ctx) 20 | o.result <- err 21 | close(o.result) 22 | }() 23 | return o 24 | } 25 | 26 | func (o *once) yield(ctx context.Context) error { 27 | select { 28 | case err := <-o.result: 29 | return err 30 | case o.execCtx <- ctx: 31 | return o.wait(ctx) 32 | case <-ctx.Done(): 33 | return ctx.Err() 34 | } 35 | } 36 | 37 | func (o *once) wait(ctx context.Context) error { 38 | select { 39 | case err := <-o.result: 40 | return err 41 | case <-ctx.Done(): 42 | return ctx.Err() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /controllers/ingress/once_test.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "sync" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestOnce(t *testing.T) { 16 | var callCount int32 17 | var errSeen int32 18 | 19 | o := newOnce(func(_ context.Context) error { 20 | _ = atomic.AddInt32(&callCount, 1) 21 | time.Sleep(time.Second) 22 | return fmt.Errorf("ERROR") 23 | }) 24 | 25 | ctx := context.Background() 26 | var wg sync.WaitGroup 27 | 28 | iters := 100 29 | wg.Add(iters) 30 | for i := 0; i < iters; i++ { 31 | go func(_ int) { 32 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(10)+10)) 33 | if err := o.yield(ctx); err != nil { 34 | _ = atomic.AddInt32(&errSeen, 1) 35 | } 36 | wg.Done() 37 | }(i) 38 | } 39 | wg.Wait() 40 | assert.Equal(t, callCount, int32(1)) 41 | assert.Equal(t, errSeen, int32(1)) 42 | } 43 | -------------------------------------------------------------------------------- /controllers/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Package mock_test contains mock clients for testing 2 | package mock_test 3 | 4 | //go:generate go run go.uber.org/mock/mockgen -package mock_test -destination client.go sigs.k8s.io/controller-runtime/pkg/client Client 5 | //go:generate go run go.uber.org/mock/mockgen -package mock_test -destination pomerium_ingress_reconciler.go github.com/pomerium/ingress-controller/pomerium IngressReconciler 6 | //go:generate go run go.uber.org/mock/mockgen -package mock_test -destination pomerium_config_reconciler.go github.com/pomerium/ingress-controller/pomerium ConfigReconciler 7 | -------------------------------------------------------------------------------- /controllers/mock/pomerium_config_reconciler.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/pomerium/ingress-controller/pomerium (interfaces: ConfigReconciler) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -package mock_test -destination pomerium_config_reconciler.go github.com/pomerium/ingress-controller/pomerium ConfigReconciler 7 | // 8 | 9 | // Package mock_test is a generated GoMock package. 10 | package mock_test 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | model "github.com/pomerium/ingress-controller/model" 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockConfigReconciler is a mock of ConfigReconciler interface. 21 | type MockConfigReconciler struct { 22 | ctrl *gomock.Controller 23 | recorder *MockConfigReconcilerMockRecorder 24 | } 25 | 26 | // MockConfigReconcilerMockRecorder is the mock recorder for MockConfigReconciler. 27 | type MockConfigReconcilerMockRecorder struct { 28 | mock *MockConfigReconciler 29 | } 30 | 31 | // NewMockConfigReconciler creates a new mock instance. 32 | func NewMockConfigReconciler(ctrl *gomock.Controller) *MockConfigReconciler { 33 | mock := &MockConfigReconciler{ctrl: ctrl} 34 | mock.recorder = &MockConfigReconcilerMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockConfigReconciler) EXPECT() *MockConfigReconcilerMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // SetConfig mocks base method. 44 | func (m *MockConfigReconciler) SetConfig(arg0 context.Context, arg1 *model.Config) (bool, error) { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "SetConfig", arg0, arg1) 47 | ret0, _ := ret[0].(bool) 48 | ret1, _ := ret[1].(error) 49 | return ret0, ret1 50 | } 51 | 52 | // SetConfig indicates an expected call of SetConfig. 53 | func (mr *MockConfigReconcilerMockRecorder) SetConfig(arg0, arg1 any) *gomock.Call { 54 | mr.mock.ctrl.T.Helper() 55 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetConfig", reflect.TypeOf((*MockConfigReconciler)(nil).SetConfig), arg0, arg1) 56 | } 57 | -------------------------------------------------------------------------------- /controllers/mock/pomerium_ingress_reconciler.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/pomerium/ingress-controller/pomerium (interfaces: IngressReconciler) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -package mock_test -destination pomerium_ingress_reconciler.go github.com/pomerium/ingress-controller/pomerium IngressReconciler 7 | // 8 | 9 | // Package mock_test is a generated GoMock package. 10 | package mock_test 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | model "github.com/pomerium/ingress-controller/model" 17 | gomock "go.uber.org/mock/gomock" 18 | types "k8s.io/apimachinery/pkg/types" 19 | ) 20 | 21 | // MockIngressReconciler is a mock of IngressReconciler interface. 22 | type MockIngressReconciler struct { 23 | ctrl *gomock.Controller 24 | recorder *MockIngressReconcilerMockRecorder 25 | } 26 | 27 | // MockIngressReconcilerMockRecorder is the mock recorder for MockIngressReconciler. 28 | type MockIngressReconcilerMockRecorder struct { 29 | mock *MockIngressReconciler 30 | } 31 | 32 | // NewMockIngressReconciler creates a new mock instance. 33 | func NewMockIngressReconciler(ctrl *gomock.Controller) *MockIngressReconciler { 34 | mock := &MockIngressReconciler{ctrl: ctrl} 35 | mock.recorder = &MockIngressReconcilerMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use. 40 | func (m *MockIngressReconciler) EXPECT() *MockIngressReconcilerMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // Delete mocks base method. 45 | func (m *MockIngressReconciler) Delete(arg0 context.Context, arg1 types.NamespacedName) (bool, error) { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "Delete", arg0, arg1) 48 | ret0, _ := ret[0].(bool) 49 | ret1, _ := ret[1].(error) 50 | return ret0, ret1 51 | } 52 | 53 | // Delete indicates an expected call of Delete. 54 | func (mr *MockIngressReconcilerMockRecorder) Delete(arg0, arg1 any) *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockIngressReconciler)(nil).Delete), arg0, arg1) 57 | } 58 | 59 | // Set mocks base method. 60 | func (m *MockIngressReconciler) Set(arg0 context.Context, arg1 []*model.IngressConfig) (bool, error) { 61 | m.ctrl.T.Helper() 62 | ret := m.ctrl.Call(m, "Set", arg0, arg1) 63 | ret0, _ := ret[0].(bool) 64 | ret1, _ := ret[1].(error) 65 | return ret0, ret1 66 | } 67 | 68 | // Set indicates an expected call of Set. 69 | func (mr *MockIngressReconcilerMockRecorder) Set(arg0, arg1 any) *gomock.Call { 70 | mr.mock.ctrl.T.Helper() 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockIngressReconciler)(nil).Set), arg0, arg1) 72 | } 73 | 74 | // Upsert mocks base method. 75 | func (m *MockIngressReconciler) Upsert(arg0 context.Context, arg1 *model.IngressConfig) (bool, error) { 76 | m.ctrl.T.Helper() 77 | ret := m.ctrl.Call(m, "Upsert", arg0, arg1) 78 | ret0, _ := ret[0].(bool) 79 | ret1, _ := ret[1].(error) 80 | return ret0, ret1 81 | } 82 | 83 | // Upsert indicates an expected call of Upsert. 84 | func (mr *MockIngressReconcilerMockRecorder) Upsert(arg0, arg1 any) *gomock.Call { 85 | mr.mock.ctrl.T.Helper() 86 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockIngressReconciler)(nil).Upsert), arg0, arg1) 87 | } 88 | -------------------------------------------------------------------------------- /controllers/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-multierror" 7 | networkingv1 "k8s.io/api/networking/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | "sigs.k8s.io/controller-runtime/pkg/log" 10 | 11 | icsv1 "github.com/pomerium/ingress-controller/apis/ingress/v1" 12 | ) 13 | 14 | // MultiIngressStatusReporter dispatches updates over multiple reporters 15 | type MultiIngressStatusReporter []IngressStatusReporter 16 | 17 | // MultiPomeriumStatusReporter dispatches updates over multiple reporters 18 | type MultiPomeriumStatusReporter []PomeriumReporter 19 | 20 | func logErrorIfAny(ctx context.Context, err error, kvs ...any) { 21 | if err == nil { 22 | return 23 | } 24 | log.FromContext(ctx).Error(err, "posting status updates", kvs...) 25 | } 26 | 27 | // IngressReconciled an ingress was successfully reconciled with Pomerium 28 | func (r MultiIngressStatusReporter) IngressReconciled(ctx context.Context, ingress *networkingv1.Ingress) { 29 | var errs *multierror.Error 30 | for _, u := range r { 31 | if err := u.IngressReconciled(ctx, ingress); err != nil { 32 | errs = multierror.Append(errs, err) 33 | } 34 | } 35 | logErrorIfAny(ctx, errs.ErrorOrNil(), "ingress", types.NamespacedName{Namespace: ingress.Namespace, Name: ingress.Name}) 36 | } 37 | 38 | // IngressNotReconciled an updated ingress resource was received, 39 | // however it could not be reconciled with Pomerium due to errors 40 | func (r MultiIngressStatusReporter) IngressNotReconciled(ctx context.Context, ingress *networkingv1.Ingress, reason error) { 41 | var errs *multierror.Error 42 | for _, u := range r { 43 | if err := u.IngressNotReconciled(ctx, ingress, reason); err != nil { 44 | errs = multierror.Append(errs, err) 45 | } 46 | } 47 | logErrorIfAny(ctx, errs.ErrorOrNil(), "ingress", types.NamespacedName{Namespace: ingress.Namespace, Name: ingress.Name}) 48 | } 49 | 50 | // IngressDeleted an ingress resource was deleted and Pomerium no longer serves it 51 | func (r MultiIngressStatusReporter) IngressDeleted(ctx context.Context, name types.NamespacedName, reason string) { 52 | var errs *multierror.Error 53 | for _, u := range r { 54 | if err := u.IngressDeleted(ctx, name, reason); err != nil { 55 | errs = multierror.Append(errs, err) 56 | } 57 | } 58 | logErrorIfAny(ctx, errs.ErrorOrNil(), "ingress", name, "original reason", reason) 59 | } 60 | 61 | // SettingsUpdated marks that configuration was reconciled 62 | func (r MultiPomeriumStatusReporter) SettingsUpdated(ctx context.Context, obj *icsv1.Pomerium) { 63 | var errs *multierror.Error 64 | for _, u := range r { 65 | if err := u.SettingsUpdated(ctx, obj); err != nil { 66 | errs = multierror.Append(errs, err) 67 | } 68 | } 69 | logErrorIfAny(ctx, errs.ErrorOrNil()) 70 | } 71 | 72 | // SettingsRejected marks that configuration was reconciled 73 | func (r MultiPomeriumStatusReporter) SettingsRejected(ctx context.Context, obj *icsv1.Pomerium, err error) { 74 | var errs *multierror.Error 75 | for _, u := range r { 76 | if err := u.SettingsRejected(ctx, obj, err); err != nil { 77 | errs = multierror.Append(errs, err) 78 | } 79 | } 80 | logErrorIfAny(ctx, errs.ErrorOrNil()) 81 | } 82 | -------------------------------------------------------------------------------- /controllers/settings/fetch.go: -------------------------------------------------------------------------------- 1 | // Package settings implements controller for Settings CRD 2 | package settings 3 | 4 | import ( 5 | context "context" 6 | "fmt" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | 12 | "github.com/pomerium/ingress-controller/model" 13 | "github.com/pomerium/ingress-controller/util" 14 | ) 15 | 16 | // FetchConfig returns 17 | func FetchConfig(ctx context.Context, client client.Client, name types.NamespacedName) (*model.Config, error) { 18 | var cfg model.Config 19 | if err := client.Get(ctx, name, &cfg.Pomerium); err != nil { 20 | return nil, fmt.Errorf("get %s: %w", name, err) 21 | } 22 | 23 | if err := fetchConfigSecrets(ctx, client, &cfg); err != nil { 24 | return &cfg, fmt.Errorf("secrets: %w", err) 25 | } 26 | 27 | if err := fetchConfigCerts(ctx, client, &cfg); err != nil { 28 | return &cfg, fmt.Errorf("certs: %w", err) 29 | } 30 | 31 | return &cfg, nil 32 | } 33 | 34 | func fetchConfigCerts(ctx context.Context, client client.Client, cfg *model.Config) error { 35 | if cfg.Certs == nil { 36 | cfg.Certs = make(map[types.NamespacedName]*corev1.Secret) 37 | } 38 | 39 | for _, src := range cfg.Spec.Certificates { 40 | name, err := util.ParseNamespacedName(src) 41 | if err != nil { 42 | return fmt.Errorf("parse %s: %w", src, err) 43 | } 44 | var secret corev1.Secret 45 | if err := client.Get(ctx, *name, &secret); err != nil { 46 | return fmt.Errorf("get %s: %w", name, err) 47 | } 48 | cfg.Certs[*name] = &secret 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func fetchConfigSecrets(ctx context.Context, client client.Client, cfg *model.Config) error { 55 | get := func(src string) func() (*corev1.Secret, error) { 56 | return func() (*corev1.Secret, error) { 57 | name, err := util.ParseNamespacedName(src) 58 | if err != nil { 59 | return nil, fmt.Errorf("parse %s: %w", src, err) 60 | } 61 | var secret corev1.Secret 62 | if err := client.Get(ctx, *name, &secret); err != nil { 63 | return nil, fmt.Errorf("get %s: %w", name, err) 64 | } 65 | return &secret, nil 66 | } 67 | } 68 | optional := func(src *string) func() (*corev1.Secret, error) { 69 | if src == nil { 70 | return func() (*corev1.Secret, error) { return nil, nil } 71 | } 72 | return get(*src) 73 | } 74 | required := func(src *string) func() (*corev1.Secret, error) { 75 | if src == nil { 76 | return func() (*corev1.Secret, error) { return nil, fmt.Errorf("required") } 77 | } 78 | return get(*src) 79 | } 80 | apply := func(name string, getFn func() (*corev1.Secret, error), dst **corev1.Secret) func() error { 81 | return func() error { 82 | secret, err := getFn() 83 | if err != nil { 84 | return fmt.Errorf("%s: %w", name, err) 85 | } 86 | if secret != nil { 87 | *dst = secret 88 | } 89 | return nil 90 | } 91 | } 92 | applyAll := func(funcs ...func() error) error { 93 | for _, fn := range funcs { 94 | if err := fn(); err != nil { 95 | return err 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | s := cfg.Spec 102 | return applyAll( 103 | apply("bootstrap secret", required(&s.Secrets), &cfg.Secrets), 104 | func() error { 105 | for _, caSecret := range s.CASecrets { 106 | secret, err := get(caSecret)() 107 | if err != nil { 108 | return fmt.Errorf("ca: %w", err) 109 | } 110 | cfg.CASecrets = append(cfg.CASecrets, secret) 111 | } 112 | return nil 113 | }, 114 | func() error { 115 | if s.IdentityProvider == nil { 116 | return nil 117 | } 118 | return applyAll( 119 | apply("secret", required(&s.IdentityProvider.Secret), &cfg.IdpSecret), 120 | apply("request params", optional(s.IdentityProvider.RequestParamsSecret), &cfg.RequestParams), 121 | apply("service account", optional(s.IdentityProvider.ServiceAccountFromSecret), &cfg.IdpServiceAccount), 122 | ) 123 | }, 124 | func() error { 125 | if s.Storage == nil { 126 | return nil 127 | } 128 | 129 | if p := s.Storage.Postgres; p != nil { 130 | if err := applyAll( 131 | apply("connection", required(&p.Secret), &cfg.StorageSecrets.Secret), 132 | apply("tls", optional(p.TLSSecret), &cfg.StorageSecrets.TLS), 133 | apply("ca", optional(p.CASecret), &cfg.StorageSecrets.CA), 134 | ); err != nil { 135 | return fmt.Errorf("postgresql: %w", err) 136 | } 137 | } else { 138 | return fmt.Errorf("if storage is specified, postgres storage should be provided") 139 | } 140 | 141 | return cfg.StorageSecrets.Validate() 142 | }, 143 | ) 144 | } 145 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | version: "0.2" 2 | language: en-US 3 | words: 4 | - apimachinery 5 | - apiserver 6 | - configmap 7 | - databroker 8 | - deepcopy 9 | - envtest 10 | - filemgr 11 | - hostnames 12 | - mockgen 13 | - oidc 14 | - otel 15 | - otlp 16 | - pomerium 17 | - protobuf 18 | - protojson 19 | - readyz 20 | - sharedkey 21 | - sslcert 22 | - sslkey 23 | - sslrootcert 24 | - uifs 25 | - unmarshaled 26 | - upsert 27 | languageSettings: 28 | - languageId: go 29 | allowCompoundWords: false 30 | ignoreRegExpList: 31 | - Urls 32 | - Base64 33 | - "/kubebuilder:.*/" 34 | - "/nolint:.*/" 35 | - "/go:.*/" 36 | includeRegExpList: 37 | - CStyleComment 38 | -------------------------------------------------------------------------------- /docs/cmd/main.go: -------------------------------------------------------------------------------- 1 | // Package main is a top level command that generates CRD documentation to the stdout 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/iancoleman/strcase" 11 | 12 | "github.com/pomerium/ingress-controller/docs" 13 | ) 14 | 15 | func main() { 16 | if err := generateMarkdown(os.Stdout); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | 21 | func generateMarkdown(w io.Writer) error { 22 | crd, err := docs.Load() 23 | if err != nil { 24 | return fmt.Errorf("loading CRD: %w", err) 25 | } 26 | 27 | tmpl, err := docs.LoadTemplates() 28 | if err != nil { 29 | return fmt.Errorf("loading templates: %w", err) 30 | } 31 | 32 | if err := tmpl.ExecuteTemplate(w, "header", nil); err != nil { 33 | return err 34 | } 35 | 36 | root := crd.Spec.Versions[0].Schema.OpenAPIV3Schema 37 | 38 | for _, key := range []string{"spec", "status"} { 39 | objects, err := docs.Flatten(key, root.Properties[key]) 40 | if err != nil { 41 | return fmt.Errorf("parsing %s: %w", key, err) 42 | } 43 | 44 | fmt.Fprintf(w, "## %s\n", strcase.ToCamel(key)) 45 | if err := tmpl.ExecuteTemplate(w, "object", objects[key]); err != nil { 46 | return fmt.Errorf("exec template: %w", err) 47 | } 48 | delete(objects, key) 49 | 50 | if err := tmpl.ExecuteTemplate(w, "objects", objects); err != nil { 51 | return fmt.Errorf("exec template: %w", err) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs generates docs from CRD specs 2 | package docs 3 | -------------------------------------------------------------------------------- /docs/known_formats.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | var ( 4 | knownFormats = map[string]string{ 5 | // standard JSON schema formats 6 | "uri": "an URI as parsed by Golang net/url.ParseRequestURI.", 7 | "hostname": "a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034].", 8 | "ipv4": "an IPv4 IP as parsed by Golang net.ParseIP.", 9 | "ipv6": "an IPv6 IP as parsed by Golang net.ParseIP.", 10 | "cidr": "a CIDR as parsed by Golang net.ParseCIDR.", 11 | "byte": "base64 encoded binary data.", 12 | "date": `a date string like "2006-01-02" as defined by full-date in RFC3339.`, 13 | "duration": `a duration string like "22s" as parsed by Golang time.ParseDuration.`, 14 | "date-time": `a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339.`, 15 | // pomerium formats 16 | "namespace/name": `reference to Kubernetes resource with namespace prefix: namespace/name format.`, 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /docs/parse.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | 8 | extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 9 | "k8s.io/apimachinery/pkg/util/yaml" 10 | 11 | "github.com/pomerium/ingress-controller/config/crd" 12 | ) 13 | 14 | // Object is a simplified representation of JSON Schema Object 15 | type Object struct { 16 | ID string 17 | Description string 18 | Properties map[string]*Property 19 | } 20 | 21 | // Property is an Object property, that may be either an atomic value, reference to an object or a map 22 | type Property struct { 23 | ID string 24 | Description string 25 | Required bool 26 | 27 | ObjectOrAtomic 28 | Map *ObjectOrAtomic 29 | } 30 | 31 | // Atomic is a base type 32 | type Atomic struct { 33 | Format string 34 | Type string 35 | } 36 | 37 | // ExplainFormat returns a human readable explanation for a known format, i.e. date-time 38 | func (a *Atomic) ExplainFormat() *string { 39 | if txt, ok := knownFormats[a.Format]; ok { 40 | return &txt 41 | } 42 | return nil 43 | } 44 | 45 | // ObjectOrAtomic represents either an object reference or an atomic value 46 | type ObjectOrAtomic struct { 47 | // ObjectRef if set, represents a reference to an object key 48 | ObjectRef *string 49 | // Atomic if set, represents an atomic type 50 | Atomic *Atomic 51 | } 52 | 53 | // Load parses CRD document from Yaml spec 54 | func Load() (*extv1.CustomResourceDefinition, error) { 55 | dec := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(crd.SettingsCRD), 100) 56 | var spec extv1.CustomResourceDefinition 57 | if err := dec.Decode(&spec); err != nil { 58 | return nil, err 59 | } 60 | return &spec, nil 61 | } 62 | 63 | // Flatten parses the JSON Schema and returns a flattened list of referenced objects 64 | func Flatten(key string, src extv1.JSONSchemaProps) (map[string]*Object, error) { 65 | objects := make(map[string]*Object) 66 | if err := flatten(key, src, objects); err != nil { 67 | return nil, err 68 | } 69 | return objects, nil 70 | } 71 | 72 | var reWS = regexp.MustCompile(`(?:[\s\n\t]|\\t|\\n)+`) 73 | 74 | func flatten(key string, src extv1.JSONSchemaProps, objects map[string]*Object) error { 75 | obj := &Object{ 76 | ID: key, 77 | Description: reWS.ReplaceAllString(src.Description, " "), 78 | Properties: make(map[string]*Property), 79 | } 80 | 81 | atomicHandler := func(_ string, prop extv1.JSONSchemaProps) (*Property, error) { 82 | return &Property{ObjectOrAtomic: ObjectOrAtomic{Atomic: atomic(prop)}}, nil 83 | } 84 | 85 | arrayHandler := func(_ string, prop extv1.JSONSchemaProps) (*Property, error) { 86 | return &Property{ObjectOrAtomic: ObjectOrAtomic{Atomic: array(prop)}}, nil 87 | } 88 | 89 | typeHandler := map[string]func(key string, prop extv1.JSONSchemaProps) (*Property, error){ 90 | "object": func(key string, prop extv1.JSONSchemaProps) (*Property, error) { 91 | if prop.AdditionalProperties != nil { 92 | // this is a map 93 | prop = *prop.AdditionalProperties.Schema 94 | if prop.Type == "object" { 95 | // register map value type under the name of this key 96 | if err := flatten(key, prop, objects); err != nil { 97 | return nil, err 98 | } 99 | return &Property{Map: &ObjectOrAtomic{ObjectRef: &key}}, nil 100 | } 101 | return &Property{Map: &ObjectOrAtomic{Atomic: atomic(prop)}}, nil 102 | } 103 | if err := flatten(key, prop, objects); err != nil { 104 | return nil, err 105 | } 106 | return &Property{ObjectOrAtomic: ObjectOrAtomic{ObjectRef: &key}}, nil 107 | }, 108 | "string": atomicHandler, 109 | "boolean": atomicHandler, 110 | "integer": atomicHandler, 111 | "array": arrayHandler, 112 | } 113 | 114 | for key, prop := range src.Properties { 115 | fn, ok := typeHandler[prop.Type] 116 | if !ok { 117 | fmt.Printf("don't know how to handle type %s\n", prop.Type) 118 | continue 119 | } 120 | val, err := fn(key, prop) 121 | if err != nil { 122 | return fmt.Errorf("%s: %w", key, err) 123 | } 124 | val.ID = key 125 | val.Description = reWS.ReplaceAllString(prop.Description, " ") 126 | obj.Properties[key] = val 127 | } 128 | 129 | for _, key := range src.Required { 130 | prop, ok := obj.Properties[key] 131 | if !ok { 132 | return fmt.Errorf("required field %s not found", key) 133 | } 134 | prop.Required = true 135 | } 136 | 137 | if _, ok := objects[key]; ok { 138 | return fmt.Errorf("cannot flatten: duplicate key %s", key) 139 | } 140 | objects[key] = obj 141 | 142 | return nil 143 | } 144 | 145 | func atomic(src extv1.JSONSchemaProps) *Atomic { 146 | return &Atomic{ 147 | Format: src.Format, 148 | Type: src.Type, 149 | } 150 | } 151 | 152 | func array(src extv1.JSONSchemaProps) *Atomic { 153 | return &Atomic{ 154 | Format: src.Format, 155 | Type: fmt.Sprintf("[]%s", src.Items.Schema.Type), 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /docs/template.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "embed" 5 | "strings" 6 | 7 | "text/template" 8 | ) 9 | 10 | //go:embed templates/*.tmpl 11 | var templateFS embed.FS 12 | 13 | // LoadTemplates would load all templates from `./templates` 14 | func LoadTemplates() (*template.Template, error) { 15 | return template.New("root").Funcs(template.FuncMap{ 16 | "anchor": strings.ToLower, 17 | }).ParseFS(templateFS, "templates/*") 18 | } 19 | -------------------------------------------------------------------------------- /docs/templates/header.tmpl: -------------------------------------------------------------------------------- 1 | {{define "header"}}--- 2 | title: Kubernetes Deployment Reference 3 | sidebar_label: Reference 4 | description: Reference for Pomerium settings in Kubernetes deployments. 5 | --- 6 | 7 | Pomerium-specific parameters should be configured via the `ingress.pomerium.io/Pomerium` CRD. 8 | The default Pomerium deployment is listening to the CRD `global`, that may be customized via command line parameters. 9 | 10 | Pomerium posts updates to the CRD `/status`: 11 | ```shell 12 | kubectl describe pomerium 13 | ``` 14 | 15 | Kubernetes-specific deployment parameters should be added via `kustomize` to the manifests. 16 | 17 | {{end}} 18 | -------------------------------------------------------------------------------- /docs/templates/object-properties.tmpl: -------------------------------------------------------------------------------- 1 | {{define "object-properties"}}{{if .}} 2 | 3 | 4 | 5 | 6 | {{range .}} 7 | 8 | 34 | 35 | {{end}} 36 | 37 |
9 |

10 | {{.ID}}   11 | {{if .ObjectRef}} 12 | object  13 | ({{.ObjectRef}}) 14 | {{else if and .Atomic .Atomic.ExplainFormat}} 15 | {{.Atomic.Type}}  16 | ({{.Atomic.Format}}) 17 | {{else if .Atomic}} 18 | {{.Atomic.Type}}  19 | {{else if .Map.Atomic}} 20 | map[string]{{.Map.Atomic.Type}} 21 | {{else if .Map.ObjectRef}} 22 | map[string] 23 | {{.Map.ObjectRef}} 24 | {{end}} 25 |

26 |

27 | {{if .Required}}Required. {{end}} 28 | {{.Description}} 29 |

30 | {{if and .Atomic .Atomic.ExplainFormat}} 31 | Format: {{.Atomic.ExplainFormat}} 32 | {{end}} 33 |
38 | {{end}}{{end}} 39 | -------------------------------------------------------------------------------- /docs/templates/object.tmpl: -------------------------------------------------------------------------------- 1 | {{define "object"}} 2 | {{.Description}} 3 | {{template "object-properties" .Properties}} 4 | {{end}} 5 | -------------------------------------------------------------------------------- /docs/templates/objects.tmpl: -------------------------------------------------------------------------------- 1 | {{define "objects"}} 2 | {{range .}} 3 | ### `{{.ID}}` 4 | {{template "object" .}} 5 | {{end}} 6 | {{end}} 7 | -------------------------------------------------------------------------------- /internal/.gitignore: -------------------------------------------------------------------------------- 1 | ui 2 | -------------------------------------------------------------------------------- /internal/filemgr/filemgr.go: -------------------------------------------------------------------------------- 1 | // Package filemgr contains a manager for files based on byte slices. 2 | package filemgr 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/martinlindhe/base36" 10 | 11 | "github.com/pomerium/pomerium/pkg/cryptutil" 12 | ) 13 | 14 | // A Manager manages temporary files created from data. 15 | type Manager struct { 16 | cacheDir string 17 | } 18 | 19 | // New creates a new Manager. 20 | func New(cacheDir string) *Manager { 21 | return &Manager{ 22 | cacheDir: cacheDir, 23 | } 24 | } 25 | 26 | // CreateFile creates a new file based on the passed in data. 27 | func (mgr *Manager) CreateFile(fileName string, data []byte) (filePath string, err error) { 28 | h := base36.EncodeBytes(cryptutil.Hash("filemgr", data)) 29 | ext := filepath.Ext(fileName) 30 | fileName = fmt.Sprintf("%s-%x%s", fileName[:len(fileName)-len(ext)], h, ext) 31 | filePath = filepath.Join(mgr.cacheDir, fileName) 32 | 33 | if err := os.MkdirAll(mgr.cacheDir, 0o700); err != nil { 34 | return filePath, fmt.Errorf("filemgr: error creating cache directory: %w", err) 35 | } 36 | 37 | _, err = os.Stat(filePath) 38 | if err == nil { 39 | return filePath, nil 40 | } 41 | 42 | err = os.WriteFile(filePath, data, 0o600) 43 | if err != nil { 44 | _ = os.Remove(filePath) 45 | return filePath, fmt.Errorf("filemgr: error writing file: %w", err) 46 | } 47 | 48 | err = os.Chmod(filePath, 0o400) 49 | if err != nil { 50 | _ = os.Remove(filePath) 51 | return filePath, fmt.Errorf("filemgr: error chmoding file: %w", err) 52 | } 53 | 54 | return filePath, nil 55 | } 56 | 57 | // DeleteFiles deletes all the files managed by the file manager. 58 | func (mgr *Manager) DeleteFiles() error { 59 | if _, err := os.Stat(mgr.cacheDir); os.IsNotExist(err) { 60 | return nil 61 | } 62 | 63 | return filepath.Walk(mgr.cacheDir, func(p string, fi os.FileInfo, err error) error { 64 | if err != nil { 65 | return err 66 | } 67 | if !fi.IsDir() { 68 | return os.Remove(p) 69 | } 70 | return nil 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /internal/filemgr/filemgr_test.go: -------------------------------------------------------------------------------- 1 | package filemgr 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestManager(t *testing.T) { 14 | dir := filepath.Join(os.TempDir(), uuid.New().String()) 15 | defer os.RemoveAll(dir) 16 | 17 | mgr := New(dir) 18 | fp1, err := mgr.CreateFile("hello.txt", []byte("HELLO WORLD")) 19 | assert.NoError(t, err) 20 | assert.Equal(t, filepath.Join(dir, "hello-32474a4f41355432494e594e58334e4b4b4453483555314e4842584544424139375148533858303543434f4e56524b43374a.txt"), fp1) 21 | 22 | fp2, err := mgr.CreateFile("empty", nil) 23 | assert.NoError(t, err) 24 | assert.Equal(t, filepath.Join(dir, "empty-314a323947555a5055304f45304944514c4f5242384244493339453533505551393131494e484f545353425a443759435453"), fp2) 25 | 26 | assert.Equal(t, 2, countFiles(dir)) 27 | assert.NoError(t, mgr.DeleteFiles()) 28 | assert.Equal(t, 0, countFiles(dir)) 29 | } 30 | 31 | func countFiles(dir string) int { 32 | fileCount := 0 33 | filepath.Walk(dir, func(_ string, info fs.FileInfo, _ error) error { 34 | if !info.IsDir() { 35 | fileCount++ 36 | } 37 | return nil 38 | }) 39 | return fileCount 40 | } 41 | -------------------------------------------------------------------------------- /internal/init.go: -------------------------------------------------------------------------------- 1 | // Package internal implements few hacks to allow pomerium embedding 2 | package internal 3 | -------------------------------------------------------------------------------- /internal/init_embed.go: -------------------------------------------------------------------------------- 1 | //go:build embed_pomerium 2 | 3 | package internal 4 | 5 | import ( 6 | "embed" 7 | "io/fs" 8 | 9 | "github.com/pomerium/pomerium/ui" 10 | ) 11 | 12 | var ( 13 | //go:embed ui/dist 14 | uiFS embed.FS 15 | ) 16 | 17 | func init() { 18 | f, err := fs.Sub(uiFS, "ui") 19 | if err != nil { 20 | panic(err) 21 | } 22 | ui.ExtUIFS = f 23 | } 24 | -------------------------------------------------------------------------------- /internal/stress/echo.go: -------------------------------------------------------------------------------- 1 | package stress 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | // RunHTTPEchoServer runs a HTTP server that responds with a 200 OK to all requests 12 | func RunHTTPEchoServer(ctx context.Context, addr string) error { 13 | log := zerolog.Ctx(ctx).With().Str("addr", addr).Logger() 14 | log.Info().Msg("starting echo server...") 15 | s := &http.Server{ 16 | Addr: addr, 17 | ReadHeaderTimeout: 10 * time.Second, 18 | Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 19 | w.WriteHeader(http.StatusOK) 20 | _, _ = w.Write([]byte("OK\n")) 21 | }), 22 | } 23 | go func() { 24 | <-ctx.Done() 25 | log.Info().Msg("stopping echo server...") 26 | shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 27 | _ = s.Shutdown(shutdownCtx) 28 | cancel() 29 | }() 30 | err := s.ListenAndServe() 31 | if err != nil && err != http.ErrServerClosed { 32 | log.Err(err).Msg("echo server terminated with error") 33 | return err 34 | } 35 | log.Info().Msg("echo server stopped") 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/stress/traffic.go: -------------------------------------------------------------------------------- 1 | package stress 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/cenkalti/backoff/v4" 10 | "github.com/rs/zerolog" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | // AwaitReadyMulti concurrently waits for multiple HTTP servers to respond with a given status code to a given URL 15 | func AwaitReadyMulti(ctx context.Context, client *http.Client, urls []string, expectHeaders map[string]string) error { 16 | eg, ctx := errgroup.WithContext(ctx) 17 | 18 | for _, url := range urls { 19 | url := url 20 | eg.Go(func() error { 21 | return AwaitReady(ctx, client, url, expectHeaders) 22 | }) 23 | } 24 | return eg.Wait() 25 | } 26 | 27 | // AwaitReady waits for a HTTP server to respond with a given status code to a given URL 28 | func AwaitReady(ctx context.Context, client *http.Client, url string, expectHeaders map[string]string) error { 29 | bo := backoff.NewExponentialBackOff() 30 | bo.MaxElapsedTime = 0 31 | 32 | for { 33 | select { 34 | case <-ctx.Done(): 35 | return ctx.Err() 36 | case <-time.After(bo.NextBackOff()): 37 | } 38 | 39 | err := tryRequest(ctx, client, url, expectHeaders) 40 | if err == nil { 41 | return nil 42 | } 43 | zerolog.Ctx(ctx).Error().Err(err).Str("url", url).Msg("waiting for status") 44 | } 45 | } 46 | 47 | func tryRequest(ctx context.Context, client *http.Client, url string, expectHeaders map[string]string) error { 48 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 49 | if err != nil { 50 | return fmt.Errorf("new request: %w", err) 51 | } 52 | 53 | resp, err := client.Do(req) 54 | if err != nil { 55 | return err 56 | } 57 | _ = resp.Body.Close() 58 | if resp.StatusCode/100 != 2 { 59 | return fmt.Errorf("unexpected status code: %d", resp.StatusCode) 60 | } 61 | 62 | for k, v := range expectHeaders { 63 | if resp.Header.Get(k) != v { 64 | return fmt.Errorf("unexpected header value for %s: want %s, got %s", k, v, resp.Header.Get(k)) 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main contains main app entry point 2 | package main 3 | 4 | import ( 5 | "log" 6 | 7 | _ "k8s.io/client-go/plugin/pkg/client/auth/azure" 8 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 9 | _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" 10 | 11 | "github.com/pomerium/ingress-controller/cmd" 12 | _ "github.com/pomerium/ingress-controller/internal" 13 | ) 14 | 15 | func main() { 16 | c, err := cmd.RootCommand() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | if err = c.Execute(); err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /model/gateway_config.go: -------------------------------------------------------------------------------- 1 | // Package model contains common data structures between the controller and pomerium config reconciler 2 | package model 3 | 4 | import ( 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/types" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 9 | 10 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 11 | ) 12 | 13 | // GatewayConfig represents the entirety of the Gateway-defined configuration. 14 | type GatewayConfig struct { 15 | Routes []GatewayHTTPRouteConfig 16 | Certificates []*corev1.Secret 17 | ExtensionFilters map[ExtensionFilterKey]ExtensionFilter 18 | } 19 | 20 | // GatewayHTTPRouteConfig represents a single Gateway-defined route together 21 | // with all objects needed to translate it into Pomerium routes. 22 | type GatewayHTTPRouteConfig struct { 23 | *gateway_v1.HTTPRoute 24 | 25 | // Hostnames this route should match. This may differ from the list of Hostnames in the 26 | // HTTPRoute Spec depending on the Gateway configuration. "All" is represented as "*". 27 | Hostnames []gateway_v1.Hostname 28 | 29 | // ValidBackendRefs determines which BackendRefs are allowed to be used for route "To" URLs. 30 | ValidBackendRefs BackendRefChecker 31 | 32 | // Services is a map of all known services in the cluster. 33 | Services map[types.NamespacedName]*corev1.Service 34 | } 35 | 36 | // BackendRefChecker is used to determine which BackendRefs are valid. 37 | type BackendRefChecker interface { 38 | Valid(obj client.Object, r *gateway_v1.BackendRef) bool 39 | } 40 | 41 | // ExtensionFilter represents a custom Pomerium route filter. 42 | type ExtensionFilter interface { 43 | ApplyToRoute(*pb.Route) 44 | } 45 | 46 | // ExtensionFilterKey is a look-up key for available custom filters. 47 | type ExtensionFilterKey struct { 48 | Kind string 49 | Namespace string 50 | Name string 51 | } 52 | -------------------------------------------------------------------------------- /model/registry.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/types" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 11 | ) 12 | 13 | // Key is dependency key 14 | type Key struct { 15 | Kind string 16 | types.NamespacedName 17 | } 18 | 19 | func (k *Key) String() string { 20 | return fmt.Sprintf("%s:%s/%s", k.Kind, k.Namespace, k.Name) 21 | } 22 | 23 | // ObjectKey returns a registry key for a given kubernetes object 24 | // the object must be properly initialized (GVK, name, namespace) 25 | func ObjectKey(obj client.Object, scheme *runtime.Scheme) Key { 26 | name := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()} 27 | gvk, err := apiutil.GVKForObject(obj, scheme) 28 | if err != nil { 29 | panic(err) 30 | } 31 | kind := gvk.Kind 32 | if kind == "" { 33 | panic("no kind available for object") 34 | } 35 | return Key{Kind: kind, NamespacedName: name} 36 | } 37 | 38 | // Registry is used to keep track of dependencies between kubernetes objects 39 | // i.e. ingress depends on secret and service configurations 40 | // no dependency subordination is tracked 41 | type Registry interface { 42 | // Add registers a dependency between x,y 43 | Add(x, y Key) 44 | // Deps returns list of dependencies given object key has 45 | Deps(x Key) []Key 46 | DepsOfKind(x Key, kind string) []Key 47 | // DeleteCascade deletes key x and also any dependent keys that do not have other dependencies 48 | DeleteCascade(x Key) 49 | } 50 | 51 | type registryItems map[Key]map[Key]bool 52 | 53 | type registry struct { 54 | sync.RWMutex 55 | items registryItems 56 | } 57 | 58 | // NewRegistry creates an empty registry safe for concurrent use 59 | func NewRegistry() Registry { 60 | return ®istry{ 61 | items: make(registryItems), 62 | } 63 | } 64 | 65 | // Add registers dependency between x and y 66 | func (r *registry) Add(x, y Key) { 67 | if x == y { 68 | return 69 | } 70 | 71 | r.Lock() 72 | defer r.Unlock() 73 | 74 | r.items.add(x, y) 75 | r.items.add(y, x) 76 | } 77 | 78 | func (r registryItems) add(x, y Key) { 79 | rx := r[x] 80 | if rx == nil { 81 | rx = make(map[Key]bool) 82 | r[x] = rx 83 | } 84 | rx[y] = true 85 | } 86 | 87 | func (r registryItems) del(x, y Key) { 88 | rx := r[x] 89 | delete(rx, y) 90 | if len(rx) == 0 { 91 | delete(r, x) 92 | } 93 | } 94 | 95 | // Deps returns list of objects that are dependent 96 | func (r *registry) Deps(x Key) []Key { 97 | r.RLock() 98 | defer r.RUnlock() 99 | 100 | rx := r.items[x] 101 | keys := make([]Key, 0, len(rx)) 102 | for k := range rx { 103 | keys = append(keys, k) 104 | } 105 | return keys 106 | } 107 | 108 | // DepsOfKind returns list of objects that are dependent and are of a particular kind 109 | func (r *registry) DepsOfKind(x Key, kind string) []Key { 110 | r.RLock() 111 | defer r.RUnlock() 112 | 113 | rx := r.items[x] 114 | keys := make([]Key, 0, len(rx)) 115 | for k := range rx { 116 | if k.Kind == kind { 117 | keys = append(keys, k) 118 | } 119 | } 120 | return keys 121 | } 122 | 123 | func (r *registry) DeleteCascade(x Key) { 124 | r.Lock() 125 | defer r.Unlock() 126 | 127 | for k := range r.items[x] { 128 | r.items.del(x, k) 129 | r.items.del(k, x) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /model/registry_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | func TestRegistry(t *testing.T) { 11 | r := NewRegistry() 12 | a := Key{Kind: "a", NamespacedName: types.NamespacedName{Name: "a", Namespace: "a"}} 13 | b := Key{Kind: "b", NamespacedName: types.NamespacedName{Name: "b", Namespace: "b"}} 14 | c := Key{Kind: "c", NamespacedName: types.NamespacedName{Name: "c", Namespace: "c"}} 15 | d := Key{Kind: "d", NamespacedName: types.NamespacedName{Name: "d", Namespace: "d"}} 16 | 17 | r.Add(a, a) 18 | r.Add(a, b) 19 | r.Add(a, c) 20 | r.Add(c, d) 21 | 22 | assert.ElementsMatch(t, []Key{b, c}, r.Deps(a)) 23 | assert.ElementsMatch(t, []Key{a}, r.Deps(b)) 24 | assert.ElementsMatch(t, []Key{a}, r.DepsOfKind(b, "a")) 25 | assert.ElementsMatch(t, []Key{a, d}, r.Deps(c)) 26 | r.DeleteCascade(c) 27 | assert.ElementsMatch(t, []Key{b}, r.Deps(a)) 28 | assert.Empty(t, r.Deps(d)) 29 | r.DeleteCascade(a) 30 | if !assert.Empty(t, r.(*registry).items) { 31 | t.Logf("%+v", r) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pomerium/cert_map.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "fmt" 8 | "sort" 9 | "strings" 10 | 11 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 12 | ) 13 | 14 | type domainKey struct { 15 | Host, Domain string 16 | } 17 | 18 | func parseDomainKey(dnsName string) domainKey { 19 | parts := strings.SplitN(dnsName, ".", 2) 20 | if len(parts) != 2 { 21 | return domainKey{Host: dnsName} 22 | } 23 | return domainKey{Host: parts[0], Domain: parts[1]} 24 | } 25 | 26 | type certRef struct { 27 | inUse bool 28 | data *pb.Settings_Certificate 29 | cert *x509.Certificate 30 | } 31 | 32 | func parseCert(cert *pb.Settings_Certificate) (*x509.Certificate, error) { 33 | block, _ := pem.Decode(cert.CertBytes) 34 | if block == nil { 35 | return nil, fmt.Errorf("failed to decode cert block") 36 | } 37 | 38 | if block.Type != "CERTIFICATE" { 39 | return nil, fmt.Errorf("expected CERTIFICATE PEM block, got %q", block.Type) 40 | } 41 | 42 | return x509.ParseCertificate(block.Bytes) 43 | } 44 | 45 | type domainMap map[domainKey]*certRef 46 | 47 | func toDomainMap(certs []*pb.Settings_Certificate) (domainMap, error) { 48 | domains := make(domainMap) 49 | for _, cert := range certs { 50 | crt, err := parseCert(cert) 51 | if err != nil { 52 | return nil, err 53 | } 54 | domains.add(cert, crt) 55 | } 56 | return domains, nil 57 | } 58 | 59 | func (dm domainMap) getCertsInUse() []*pb.Settings_Certificate { 60 | certMap := make(map[*pb.Settings_Certificate]struct{}) 61 | for _, ref := range dm { 62 | if ref.inUse { 63 | certMap[ref.data] = struct{}{} 64 | } 65 | } 66 | certs := make(byCert, 0, len(certMap)) 67 | for crt := range certMap { 68 | certs = append(certs, crt) 69 | } 70 | sort.Sort(certs) 71 | return certs 72 | } 73 | 74 | func (dm domainMap) addIfNewer(key domainKey, ref *certRef) { 75 | cur := dm[key] 76 | if cur == nil { 77 | dm[key] = ref 78 | return 79 | } 80 | if cur.cert.NotAfter.Before(ref.cert.NotAfter) { 81 | dm[key] = ref 82 | } 83 | } 84 | 85 | func (dm domainMap) add(data *pb.Settings_Certificate, cert *x509.Certificate) { 86 | ref := &certRef{ 87 | inUse: false, 88 | data: data, 89 | cert: cert, 90 | } 91 | for _, name := range cert.DNSNames { 92 | dm.addIfNewer(parseDomainKey(name), ref) 93 | } 94 | } 95 | 96 | func (dm domainMap) markInUse(dnsName string) { 97 | key := parseDomainKey(dnsName) 98 | if ref := dm[key]; ref != nil { 99 | ref.inUse = true 100 | return 101 | } 102 | if ref := dm[domainKey{Host: "*", Domain: key.Domain}]; ref != nil { 103 | ref.inUse = true 104 | } 105 | } 106 | 107 | type byCert []*pb.Settings_Certificate 108 | 109 | func (a byCert) Len() int { return len(a) } 110 | func (a byCert) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 111 | func (a byCert) Less(i, j int) bool { return bytes.Compare(a[i].CertBytes, a[j].CertBytes) < 0 } 112 | -------------------------------------------------------------------------------- /pomerium/certs.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | 10 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 11 | ) 12 | 13 | func addCerts(cfg *pb.Config, secrets map[types.NamespacedName]*corev1.Secret) { 14 | if cfg.Settings == nil { 15 | cfg.Settings = new(pb.Settings) 16 | } 17 | 18 | for _, secret := range secrets { 19 | if secret.Type != corev1.SecretTypeTLS { 20 | continue 21 | } 22 | addTLSCert(cfg.Settings, secret) 23 | } 24 | } 25 | 26 | func addTLSCert(s *pb.Settings, secret *corev1.Secret) { 27 | s.Certificates = append(s.Certificates, &pb.Settings_Certificate{ 28 | CertBytes: secret.Data[corev1.TLSCertKey], 29 | KeyBytes: secret.Data[corev1.TLSPrivateKeyKey], 30 | }) 31 | } 32 | 33 | func removeUnusedCerts(cfg *pb.Config) error { 34 | if cfg.Settings == nil { 35 | return nil 36 | } 37 | 38 | dm, err := toDomainMap(cfg.Settings.Certificates) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | domains, err := getAllDomains(cfg) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | for domain := range domains { 49 | dm.markInUse(domain) 50 | } 51 | 52 | cfg.Settings.Certificates = dm.getCertsInUse() 53 | return nil 54 | } 55 | 56 | func getAllDomains(cfg *pb.Config) (map[string]struct{}, error) { 57 | domains := make(map[string]struct{}) 58 | for _, r := range cfg.Routes { 59 | u, err := url.Parse(r.From) 60 | if err != nil { 61 | return nil, fmt.Errorf("cannot parse from=%s: %w", r.From, err) 62 | } 63 | domains[u.Hostname()] = struct{}{} 64 | } 65 | if cfg.Settings != nil && cfg.Settings.AuthenticateServiceUrl != nil { 66 | u, err := url.Parse(*cfg.Settings.AuthenticateServiceUrl) 67 | if err != nil { 68 | return nil, fmt.Errorf("cannot parse authenticate_service_url=%s: %w", *cfg.Settings.AuthenticateServiceUrl, err) 69 | } 70 | 71 | domains[u.Hostname()] = struct{}{} 72 | } 73 | return domains, nil 74 | } 75 | -------------------------------------------------------------------------------- /pomerium/config_test.go: -------------------------------------------------------------------------------- 1 | package pomerium_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/stretchr/testify/assert" 9 | "google.golang.org/protobuf/proto" 10 | "google.golang.org/protobuf/testing/protocmp" 11 | 12 | v1 "github.com/pomerium/ingress-controller/apis/ingress/v1" 13 | "github.com/pomerium/ingress-controller/model" 14 | "github.com/pomerium/ingress-controller/pomerium" 15 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 16 | ) 17 | 18 | func TestApplyConfig_DownstreamMTLS(t *testing.T) { 19 | ctx := context.Background() 20 | 21 | for _, tc := range []struct { 22 | name string 23 | expect *pb.DownstreamMtlsSettings 24 | mtls *v1.DownstreamMTLS 25 | }{ 26 | {"nil", nil, nil}, 27 | {"empty", &pb.DownstreamMtlsSettings{}, &v1.DownstreamMTLS{}}, 28 | { 29 | "ca", 30 | &pb.DownstreamMtlsSettings{Ca: proto.String("AQIDBA==")}, 31 | &v1.DownstreamMTLS{CA: []byte{1, 2, 3, 4}}, 32 | }, 33 | { 34 | "crl", 35 | &pb.DownstreamMtlsSettings{Crl: proto.String("BQYHCA==")}, 36 | &v1.DownstreamMTLS{CRL: []byte{5, 6, 7, 8}}, 37 | }, 38 | { 39 | "policy_with_default_deny", 40 | &pb.DownstreamMtlsSettings{Enforcement: pb.MtlsEnforcementMode_POLICY_WITH_DEFAULT_DENY.Enum()}, 41 | &v1.DownstreamMTLS{Enforcement: proto.String("policy_with_default_deny")}, 42 | }, 43 | { 44 | "policy", 45 | &pb.DownstreamMtlsSettings{Enforcement: pb.MtlsEnforcementMode_POLICY.Enum()}, 46 | &v1.DownstreamMTLS{Enforcement: proto.String("policy")}, 47 | }, 48 | { 49 | "reject_connection", 50 | &pb.DownstreamMtlsSettings{Enforcement: pb.MtlsEnforcementMode_REJECT_CONNECTION.Enum()}, 51 | &v1.DownstreamMTLS{Enforcement: proto.String("REJECT_CONNECTION")}, 52 | }, 53 | { 54 | "unknown", 55 | &pb.DownstreamMtlsSettings{}, 56 | &v1.DownstreamMTLS{Enforcement: proto.String("unknown")}, 57 | }, 58 | { 59 | "dns", 60 | &pb.DownstreamMtlsSettings{MatchSubjectAltNames: []*pb.SANMatcher{{SanType: pb.SANMatcher_DNS, Pattern: "DNS"}}}, 61 | &v1.DownstreamMTLS{MatchSubjectAltNames: &v1.MatchSubjectAltNames{DNS: "DNS"}}, 62 | }, 63 | { 64 | "email", 65 | &pb.DownstreamMtlsSettings{MatchSubjectAltNames: []*pb.SANMatcher{{SanType: pb.SANMatcher_EMAIL, Pattern: "EMAIL"}}}, 66 | &v1.DownstreamMTLS{MatchSubjectAltNames: &v1.MatchSubjectAltNames{Email: "EMAIL"}}, 67 | }, 68 | { 69 | "ip address", 70 | &pb.DownstreamMtlsSettings{MatchSubjectAltNames: []*pb.SANMatcher{{SanType: pb.SANMatcher_IP_ADDRESS, Pattern: "IP_ADDRESS"}}}, 71 | &v1.DownstreamMTLS{MatchSubjectAltNames: &v1.MatchSubjectAltNames{IPAddress: "IP_ADDRESS"}}, 72 | }, 73 | { 74 | "uri", 75 | &pb.DownstreamMtlsSettings{MatchSubjectAltNames: []*pb.SANMatcher{{SanType: pb.SANMatcher_URI, Pattern: "URI"}}}, 76 | &v1.DownstreamMTLS{MatchSubjectAltNames: &v1.MatchSubjectAltNames{URI: "URI"}}, 77 | }, 78 | { 79 | "user principal name", 80 | &pb.DownstreamMtlsSettings{MatchSubjectAltNames: []*pb.SANMatcher{{SanType: pb.SANMatcher_USER_PRINCIPAL_NAME, Pattern: "USER_PRINCIPAL_NAME"}}}, 81 | &v1.DownstreamMTLS{MatchSubjectAltNames: &v1.MatchSubjectAltNames{UserPrincipalName: "USER_PRINCIPAL_NAME"}}, 82 | }, 83 | { 84 | "max verify depth", 85 | &pb.DownstreamMtlsSettings{MaxVerifyDepth: proto.Uint32(23)}, 86 | &v1.DownstreamMTLS{MaxVerifyDepth: proto.Uint32(23)}, 87 | }, 88 | } { 89 | src := &model.Config{ 90 | Pomerium: v1.Pomerium{ 91 | Spec: v1.PomeriumSpec{ 92 | DownstreamMTLS: tc.mtls, 93 | }, 94 | }, 95 | } 96 | dst := new(pb.Config) 97 | err := pomerium.ApplyConfig(ctx, dst, src) 98 | assert.NoError(t, err, 99 | "should have no error in %s", tc.name) 100 | assert.Empty(t, cmp.Diff(tc.expect, dst.Settings.DownstreamMtls, protocmp.Transform()), 101 | "should match in %s", tc.name) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pomerium/ctrl/bootstrap_test.go: -------------------------------------------------------------------------------- 1 | package ctrl 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | v1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | 13 | "github.com/pomerium/pomerium/config" 14 | 15 | "github.com/pomerium/ingress-controller/model" 16 | "github.com/pomerium/ingress-controller/util" 17 | ) 18 | 19 | func mustB64Decode(t *testing.T, txt string) []byte { 20 | t.Helper() 21 | data, err := base64.StdEncoding.DecodeString(txt) 22 | require.NoError(t, err) 23 | return data 24 | } 25 | 26 | func TestSecretsDecode(t *testing.T) { 27 | secrets, err := util.NewBootstrapSecrets(types.NamespacedName{}) 28 | require.NoError(t, err) 29 | 30 | var opts config.Options 31 | require.NoError(t, applySecrets(context.Background(), &opts, &model.Config{Secrets: secrets})) 32 | 33 | assert.Equal(t, base64.StdEncoding.EncodeToString(secrets.Data["cookie_secret"]), opts.CookieSecret) 34 | assert.Equal(t, base64.StdEncoding.EncodeToString(secrets.Data["shared_secret"]), opts.SharedKey) 35 | assert.Equal(t, base64.StdEncoding.EncodeToString(secrets.Data["signing_key"]), opts.SigningKey) 36 | } 37 | 38 | func TestSecretsDecodeRules(t *testing.T) { 39 | var opts config.Options 40 | 41 | assert.NoError(t, applySecrets(context.Background(), &opts, &model.Config{ 42 | Secrets: &v1.Secret{ 43 | Data: map[string][]byte{ 44 | "shared_secret": mustB64Decode(t, "9OkZR6hwfmVD3a7Sfmgq58lUbFJGGz4hl/R9xbHFCAg="), 45 | "cookie_secret": mustB64Decode(t, "WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg="), 46 | "signing_key": mustB64Decode(t, "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhQbkN5MXk0TEZZVkhQb3RzM05rUSttTXJLcDgvVmVWRkRwaUk2TVNxMlVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFT1h0VXAxOWFwRnNvVWJoYkI2cExMR1o1WFBXRlE5YWtmeW5ISy9RZ3paNC9MRjZhWEY2egpvS3lHMnNtL2wyajFiQ1JxUGJNd3dEVW9iWFNIODVIeDdRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="), 47 | }, 48 | }, 49 | }), "ok secret") 50 | 51 | assert.Error(t, applySecrets(context.Background(), &opts, &model.Config{})) 52 | assert.Error(t, applySecrets(context.Background(), &opts, &model.Config{ 53 | Secrets: &v1.Secret{ 54 | Data: map[string][]byte{ 55 | "cookie_secret": mustB64Decode(t, "WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg="), 56 | "signing_key": mustB64Decode(t, "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhQbkN5MXk0TEZZVkhQb3RzM05rUSttTXJLcDgvVmVWRkRwaUk2TVNxMlVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFT1h0VXAxOWFwRnNvVWJoYkI2cExMR1o1WFBXRlE5YWtmeW5ISy9RZ3paNC9MRjZhWEY2egpvS3lHMnNtL2wyajFiQ1JxUGJNd3dEVW9iWFNIODVIeDdRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="), 57 | }, 58 | }, 59 | })) 60 | assert.Error(t, applySecrets(context.Background(), &opts, &model.Config{ 61 | Secrets: &v1.Secret{ 62 | Data: map[string][]byte{ 63 | "shared_secret": {1, 2, 3}, 64 | "cookie_secret": mustB64Decode(t, "WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg="), 65 | "signing_key": mustB64Decode(t, "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhQbkN5MXk0TEZZVkhQb3RzM05rUSttTXJLcDgvVmVWRkRwaUk2TVNxMlVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFT1h0VXAxOWFwRnNvVWJoYkI2cExMR1o1WFBXRlE5YWtmeW5ISy9RZ3paNC9MRjZhWEY2egpvS3lHMnNtL2wyajFiQ1JxUGJNd3dEVW9iWFNIODVIeDdRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="), 66 | }, 67 | }, 68 | })) 69 | } 70 | -------------------------------------------------------------------------------- /pomerium/ctrl/config.go: -------------------------------------------------------------------------------- 1 | package ctrl 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/google/go-cmp/cmp/cmpopts" 9 | 10 | "github.com/pomerium/pomerium/config" 11 | ) 12 | 13 | // InMemoryConfigSource represents bootstrap config source 14 | type InMemoryConfigSource struct { 15 | mu sync.Mutex 16 | cfg *config.Config 17 | listeners []config.ChangeListener 18 | } 19 | 20 | var ( 21 | _ = config.Source(new(InMemoryConfigSource)) 22 | ) 23 | 24 | var ( 25 | cmpOpts = []cmp.Option{ 26 | cmpopts.IgnoreUnexported(config.Options{}), 27 | cmpopts.EquateEmpty(), 28 | } 29 | ) 30 | 31 | // SetConfig updates the underlying configuration 32 | // it returns true if configuration was updated 33 | // and informs config change listeners in case there was a change 34 | func (src *InMemoryConfigSource) SetConfig(ctx context.Context, cfg *config.Config) bool { 35 | src.mu.Lock() 36 | defer src.mu.Unlock() 37 | 38 | if changed := !cmp.Equal(cfg, src.cfg, cmpOpts...); !changed { 39 | return false 40 | } 41 | 42 | src.cfg = cfg.Clone() 43 | 44 | for _, l := range src.listeners { 45 | l(ctx, src.cfg) 46 | } 47 | 48 | return true 49 | } 50 | 51 | // GetConfig implements config.Source 52 | func (src *InMemoryConfigSource) GetConfig() *config.Config { 53 | src.mu.Lock() 54 | defer src.mu.Unlock() 55 | 56 | if src.cfg == nil { 57 | panic("should not be called prior to initial config available") 58 | } 59 | 60 | return src.cfg 61 | } 62 | 63 | // OnConfigChange implements config.Source 64 | func (src *InMemoryConfigSource) OnConfigChange(_ context.Context, l config.ChangeListener) { 65 | src.mu.Lock() 66 | src.listeners = append(src.listeners, l) 67 | src.mu.Unlock() 68 | } 69 | -------------------------------------------------------------------------------- /pomerium/ctrl/config_test.go: -------------------------------------------------------------------------------- 1 | package ctrl_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/pomerium/pomerium/config" 10 | 11 | "github.com/pomerium/ingress-controller/pomerium/ctrl" 12 | ) 13 | 14 | func TestConfigChangeDetect(t *testing.T) { 15 | cfg := new(ctrl.InMemoryConfigSource) 16 | 17 | ctx := context.Background() 18 | def := *config.NewDefaultOptions() 19 | for _, tc := range []struct { 20 | msg string 21 | expect bool 22 | config.Options 23 | }{ 24 | {"initial", true, def}, 25 | {"same initial", false, def}, 26 | {"same again", false, def}, 27 | {"changed", true, config.Options{}}, 28 | } { 29 | assert.Equal(t, tc.expect, cfg.SetConfig(ctx, &config.Config{Options: &tc.Options}), tc.msg) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pomerium/ctrl/run.go: -------------------------------------------------------------------------------- 1 | package ctrl 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "sigs.k8s.io/controller-runtime/pkg/log" 9 | 10 | "github.com/pomerium/pomerium/config" 11 | pomerium_cmd "github.com/pomerium/pomerium/pkg/cmd/pomerium" 12 | 13 | "github.com/pomerium/ingress-controller/model" 14 | "github.com/pomerium/ingress-controller/pomerium" 15 | ) 16 | 17 | var _ = pomerium.ConfigReconciler(new(Runner)) 18 | 19 | // Runner implements pomerium control loop 20 | type Runner struct { 21 | src *InMemoryConfigSource 22 | base config.Config 23 | sync.Once 24 | ready chan struct{} 25 | } 26 | 27 | // waitForConfig waits until initial configuration is available 28 | func (r *Runner) waitForConfig(ctx context.Context) error { 29 | select { 30 | case <-ctx.Done(): 31 | return ctx.Err() 32 | case <-r.ready: 33 | } 34 | return nil 35 | } 36 | 37 | func (r *Runner) readyToRun() { 38 | close(r.ready) 39 | } 40 | 41 | // GetConfig returns current configuration snapshot 42 | func (r *Runner) GetConfig() *config.Config { 43 | return r.src.GetConfig() 44 | } 45 | 46 | // SetConfig updates just the shared config settings 47 | func (r *Runner) SetConfig(ctx context.Context, src *model.Config) (changes bool, err error) { 48 | dst := r.base.Clone() 49 | 50 | if err := Apply(ctx, dst.Options, src); err != nil { 51 | return false, fmt.Errorf("transform config: %w", err) 52 | } 53 | 54 | changed := r.src.SetConfig(ctx, dst) 55 | r.Once.Do(r.readyToRun) 56 | 57 | return changed, nil 58 | } 59 | 60 | // NewPomeriumRunner creates new pomerium command and control 61 | func NewPomeriumRunner(base config.Config, listener config.ChangeListener) (*Runner, error) { 62 | return &Runner{ 63 | base: base, 64 | src: &InMemoryConfigSource{ 65 | listeners: []config.ChangeListener{listener}, 66 | }, 67 | ready: make(chan struct{}), 68 | }, nil 69 | } 70 | 71 | // Run starts pomerium once config is available 72 | func (r *Runner) Run(ctx context.Context) error { 73 | if err := r.waitForConfig(ctx); err != nil { 74 | return fmt.Errorf("waiting for pomerium bootstrap config: %w", err) 75 | } 76 | 77 | log.FromContext(ctx).V(1).Info("got bootstrap config, starting pomerium...", "cfg", r.src.GetConfig()) 78 | 79 | return pomerium_cmd.Run(ctx, r.src) 80 | } 81 | -------------------------------------------------------------------------------- /pomerium/envoy/envoy_darwin_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && amd64 2 | // +build darwin,amd64 3 | 4 | package envoy 5 | 6 | import _ "embed" // embed 7 | 8 | //go:embed bin/envoy-darwin-amd64 9 | var rawBinary []byte 10 | 11 | //go:embed bin/envoy-darwin-amd64.sha256 12 | var rawChecksum string 13 | 14 | //go:embed bin/envoy-darwin-amd64.version 15 | var rawVersion string 16 | -------------------------------------------------------------------------------- /pomerium/envoy/envoy_darwin_arm64.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && arm64 2 | // +build darwin,arm64 3 | 4 | package envoy 5 | 6 | import _ "embed" // embed 7 | 8 | //go:embed bin/envoy-darwin-arm64 9 | var rawBinary []byte 10 | 11 | //go:embed bin/envoy-darwin-arm64.sha256 12 | var rawChecksum string 13 | 14 | //go:embed bin/envoy-darwin-arm64.version 15 | var rawVersion string 16 | -------------------------------------------------------------------------------- /pomerium/envoy/envoy_linux_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && amd64 2 | // +build linux,amd64 3 | 4 | package envoy 5 | 6 | import _ "embed" // embed 7 | 8 | //go:embed bin/envoy-linux-amd64 9 | var rawBinary []byte 10 | 11 | //go:embed bin/envoy-linux-amd64.sha256 12 | var rawChecksum string 13 | 14 | //go:embed bin/envoy-linux-amd64.version 15 | var rawVersion string 16 | -------------------------------------------------------------------------------- /pomerium/envoy/envoy_linux_arm64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && arm64 2 | // +build linux,arm64 3 | 4 | package envoy 5 | 6 | import _ "embed" // embed 7 | 8 | //go:embed bin/envoy-linux-arm64 9 | var rawBinary []byte 10 | 11 | //go:embed bin/envoy-linux-arm64.sha256 12 | var rawChecksum string 13 | 14 | //go:embed bin/envoy-linux-arm64.version 15 | var rawVersion string 16 | -------------------------------------------------------------------------------- /pomerium/envoy/envoy_test.go: -------------------------------------------------------------------------------- 1 | //go:build embed_pomerium 2 | 3 | package envoy_test 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | envoy_config_bootstrap_v3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" 12 | envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 13 | "github.com/google/uuid" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | envoy_core "github.com/pomerium/pomerium/pkg/envoy" 18 | 19 | "github.com/pomerium/ingress-controller/pomerium/envoy" 20 | ) 21 | 22 | func TestDeletedBinary(t *testing.T) { 23 | p1, err := envoy_core.Extract() 24 | assert.NoError(t, err) 25 | 26 | err = os.Remove(p1) 27 | assert.NoError(t, err) 28 | 29 | p2, err := envoy_core.Extract() 30 | assert.NoError(t, err) 31 | 32 | assert.NotEqual(t, p1, p2) 33 | } 34 | 35 | func TestValidate(t *testing.T) { 36 | ctx, clearTimeout := context.WithTimeout(context.Background(), time.Second*10) 37 | defer clearTimeout() 38 | 39 | t.Run("valid", func(t *testing.T) { 40 | res, err := envoy.Validate(ctx, &envoy_config_bootstrap_v3.Bootstrap{}, uuid.NewString()) 41 | require.NoError(t, err) 42 | assert.True(t, res.Valid) 43 | assert.Equal(t, "OK", res.Message) 44 | }) 45 | t.Run("invalid", func(t *testing.T) { 46 | res, err := envoy.Validate(ctx, &envoy_config_bootstrap_v3.Bootstrap{ 47 | Admin: &envoy_config_bootstrap_v3.Admin{ 48 | Address: &envoy_config_core_v3.Address{ 49 | Address: &envoy_config_core_v3.Address_SocketAddress{ 50 | SocketAddress: &envoy_config_core_v3.SocketAddress{ 51 | Protocol: envoy_config_core_v3.SocketAddress_TCP, 52 | Address: "<>", 53 | PortSpecifier: &envoy_config_core_v3.SocketAddress_PortValue{ 54 | PortValue: 1234, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, uuid.NewString()) 61 | require.NoError(t, err) 62 | assert.False(t, res.Valid) 63 | assert.Contains(t, res.Message, "malformed IP address: <>") 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /pomerium/envoy/validate.go: -------------------------------------------------------------------------------- 1 | package envoy 2 | 3 | // A ValidateResult is the result of validation. 4 | type ValidateResult struct { 5 | Valid bool 6 | Message string 7 | } 8 | -------------------------------------------------------------------------------- /pomerium/envoy/validate_envoy.go: -------------------------------------------------------------------------------- 1 | //go:build embed_pomerium 2 | // +build embed_pomerium 3 | 4 | // Package envoy contains functions for working with an embedded envoy binary. 5 | package envoy 6 | 7 | import ( 8 | "context" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | 14 | envoy_config_bootstrap_v3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" 15 | "google.golang.org/protobuf/proto" 16 | 17 | "github.com/pomerium/pomerium/pkg/envoy" 18 | "github.com/pomerium/pomerium/pkg/envoy/files" 19 | ) 20 | 21 | const ( 22 | ownerRW = os.FileMode(0o600) 23 | ) 24 | 25 | func init() { 26 | files.SetFiles(rawBinary, rawChecksum, rawVersion) 27 | } 28 | 29 | // Validate validates the bootstrap envoy config. 30 | func Validate(ctx context.Context, bootstrap *envoy_config_bootstrap_v3.Bootstrap, id string) (*ValidateResult, error) { 31 | bs, err := proto.Marshal(bootstrap) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | cfgName := filepath.Join(os.TempDir(), id+".pb") 37 | err = ioutil.WriteFile(cfgName, bs, ownerRW) 38 | if err != nil { 39 | return nil, err 40 | } 41 | // remove the file when we're done 42 | defer func() { _ = os.Remove(cfgName) }() 43 | 44 | cmd, err := cmd(ctx, 45 | "--config-path", cfgName, 46 | "--mode", "validate", 47 | "--log-level", "error", 48 | "--log-format", "%v") 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | envoyBS, err := cmd.CombinedOutput() 54 | // an “OK” message (in which case the exit code is 0) 55 | // or any errors generated by the configuration file (exit code 1) 56 | if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { 57 | return &ValidateResult{ 58 | Valid: false, 59 | Message: string(envoyBS), 60 | }, nil 61 | } else if err != nil { 62 | // all other errors are returned as errors 63 | return nil, err 64 | } 65 | return &ValidateResult{ 66 | Valid: true, 67 | Message: "OK", 68 | }, nil 69 | } 70 | 71 | // cmd creates an exec.Cmd using the embedded envoy binary. 72 | func cmd(ctx context.Context, arg ...string) (*exec.Cmd, error) { 73 | fullEnvoyPath, err := envoy.Extract() 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return exec.CommandContext(ctx, fullEnvoyPath, arg...), nil 79 | } 80 | -------------------------------------------------------------------------------- /pomerium/envoy/validate_noop.go: -------------------------------------------------------------------------------- 1 | //go:build !embed_pomerium 2 | // +build !embed_pomerium 3 | 4 | // Package envoy contains functions for working with an embedded envoy binary. 5 | package envoy 6 | 7 | import ( 8 | "context" 9 | 10 | envoy_config_bootstrap_v3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" 11 | ) 12 | 13 | // Validate validates the bootstrap envoy config. 14 | func Validate(ctx context.Context, bootstrap *envoy_config_bootstrap_v3.Bootstrap, id string) (*ValidateResult, error) { 15 | return &ValidateResult{ 16 | Valid: true, 17 | Message: "NOOP VALIDATION", 18 | }, nil 19 | } 20 | -------------------------------------------------------------------------------- /pomerium/gateway/backendrefs.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "k8s.io/apimachinery/pkg/types" 8 | "k8s.io/apimachinery/pkg/util/intstr" 9 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 10 | 11 | "github.com/pomerium/ingress-controller/model" 12 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 13 | ) 14 | 15 | // applyBackendRefs translates backendRefs to a weighted set of Pomerium "To" URLs. 16 | // [applyFilters] must be called prior to this method. 17 | func applyBackendRefs( 18 | route *pb.Route, 19 | gc *model.GatewayHTTPRouteConfig, 20 | backendRefs []gateway_v1.HTTPBackendRef, 21 | ) { 22 | // From the spec: "BackendRefs defines API objects where matching requests should be sent. If 23 | // unspecified, the rule performs no forwarding. If unspecified and no filters are specified 24 | // that would result in a response being sent, a 404 error code is returned." 25 | if route.Redirect == nil && len(backendRefs) == 0 { 26 | route.Response = &pb.RouteDirectResponse{ 27 | Status: http.StatusNotFound, 28 | Body: "no backend specified", 29 | } 30 | return 31 | } 32 | 33 | for i := range backendRefs { 34 | if !gc.ValidBackendRefs.Valid(gc.HTTPRoute, &backendRefs[i].BackendRef) { 35 | continue 36 | } 37 | if u, w := backendRefToToURLAndWeight(gc, &backendRefs[i]); w > 0 { 38 | route.To = append(route.To, u) 39 | route.LoadBalancingWeights = append(route.LoadBalancingWeights, w) 40 | } 41 | } 42 | 43 | // From the spec: "If all entries in BackendRefs are invalid, and there are also no filters 44 | // specified in this route rule, all traffic which matches this rule MUST receive a 500 status 45 | // code." 46 | if route.Redirect == nil && len(route.To) == 0 { 47 | route.Response = &pb.RouteDirectResponse{ 48 | Status: http.StatusInternalServerError, 49 | Body: "no valid backend", 50 | } 51 | } 52 | } 53 | 54 | func backendRefToToURLAndWeight( 55 | gc *model.GatewayHTTPRouteConfig, 56 | br *gateway_v1.HTTPBackendRef, 57 | ) (string, uint32) { 58 | // Note: currently the only supported backendRef kind is "Service". 59 | namespace := gc.Namespace 60 | if br.Namespace != nil { 61 | namespace = string(*br.Namespace) 62 | } 63 | 64 | port := int32(*br.Port) 65 | 66 | // For a headless service we need the targetPort instead. 67 | // For now this supports only port numbers, not named ports, but this is enough to pass the 68 | // HTTPRouteServiceTypes conformance test cases. 69 | svc := gc.Services[types.NamespacedName{Namespace: namespace, Name: string(br.Name)}] 70 | if svc != nil && svc.Spec.ClusterIP == "None" { 71 | for i := range svc.Spec.Ports { 72 | p := &svc.Spec.Ports[i] 73 | if p.Port == port && p.TargetPort.Type == intstr.Int { 74 | port = p.TargetPort.IntVal 75 | break 76 | } 77 | } 78 | } 79 | 80 | u := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", br.Name, namespace, port) 81 | 82 | weight := uint32(1) 83 | if br.Weight != nil { 84 | weight = uint32(*br.Weight) //nolint:gosec 85 | } 86 | 87 | return u, weight 88 | } 89 | -------------------------------------------------------------------------------- /pomerium/gateway/filters.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/open-policy-agent/opa/ast" 8 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 9 | 10 | icgv1alpha1 "github.com/pomerium/ingress-controller/apis/gateway/v1alpha1" 11 | "github.com/pomerium/ingress-controller/model" 12 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 13 | "github.com/pomerium/pomerium/pkg/policy" 14 | ) 15 | 16 | func applyFilters( 17 | route *pb.Route, 18 | config *model.GatewayConfig, 19 | routeConfig *model.GatewayHTTPRouteConfig, 20 | filters []gateway_v1.HTTPRouteFilter, 21 | ) error { 22 | for i := range filters { 23 | if err := applyFilter(route, config, routeConfig, &filters[i]); err != nil { 24 | return err 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | func applyFilter( 31 | route *pb.Route, 32 | config *model.GatewayConfig, 33 | routeConfig *model.GatewayHTTPRouteConfig, 34 | filter *gateway_v1.HTTPRouteFilter, 35 | ) error { 36 | switch filter.Type { 37 | case gateway_v1.HTTPRouteFilterRequestHeaderModifier: 38 | applyRequestHeaderFilter(route, filter.RequestHeaderModifier) 39 | case gateway_v1.HTTPRouteFilterRequestRedirect: 40 | applyRedirectFilter(route, filter.RequestRedirect) 41 | case gateway_v1.HTTPRouteFilterExtensionRef: 42 | return applyExtensionFilter(route, config, routeConfig, filter.ExtensionRef) 43 | default: 44 | return fmt.Errorf("filter type %q not supported", filter.Type) 45 | } 46 | return nil 47 | } 48 | 49 | func applyRequestHeaderFilter(route *pb.Route, filter *gateway_v1.HTTPHeaderFilter) { 50 | // Note: "append" is not supported yet. 51 | route.SetRequestHeaders = makeHeadersMap(filter.Set) 52 | route.RemoveRequestHeaders = filter.Remove 53 | } 54 | 55 | func makeHeadersMap(headers []gateway_v1.HTTPHeader) map[string]string { 56 | if len(headers) == 0 { 57 | return nil 58 | } 59 | 60 | m := make(map[string]string) 61 | for i := range headers { 62 | m[string(headers[i].Name)] = headers[i].Value 63 | } 64 | return m 65 | } 66 | 67 | func applyRedirectFilter(route *pb.Route, filter *gateway_v1.HTTPRequestRedirectFilter) { 68 | rr := pb.RouteRedirect{ 69 | SchemeRedirect: filter.Scheme, 70 | HostRedirect: (*string)(filter.Hostname), 71 | } 72 | if filter.StatusCode != nil { 73 | code := int32(*filter.StatusCode) //nolint:gosec 74 | rr.ResponseCode = &code 75 | } 76 | if filter.Port != nil { 77 | port := uint32(*filter.Port) //nolint:gosec 78 | rr.PortRedirect = &port 79 | } 80 | route.Redirect = &rr 81 | } 82 | 83 | func applyExtensionFilter( 84 | route *pb.Route, 85 | config *model.GatewayConfig, 86 | routeConfig *model.GatewayHTTPRouteConfig, 87 | filter *gateway_v1.LocalObjectReference, 88 | ) error { 89 | // Make sure the API group is the one we expect. 90 | if filter.Group != gateway_v1.Group(icgv1alpha1.GroupVersion.Group) { 91 | return fmt.Errorf("unsupported filter group %q", filter.Group) 92 | } 93 | 94 | k := model.ExtensionFilterKey{ 95 | Kind: string(filter.Kind), 96 | Namespace: routeConfig.Namespace, 97 | Name: string(filter.Name), 98 | } 99 | f := config.ExtensionFilters[k] 100 | if f == nil { 101 | return fmt.Errorf("filter not found (%v)", k) 102 | } 103 | 104 | f.ApplyToRoute(route) 105 | return nil 106 | } 107 | 108 | // PolicyFilter applies a Pomerium policy defined by the PolicyFilter CRD. 109 | type PolicyFilter struct { 110 | rego string 111 | } 112 | 113 | // NewPolicyFilter parses a PolicyFilter CRD object, returning an error if the object is not valid. 114 | func NewPolicyFilter(obj *icgv1alpha1.PolicyFilter) (*PolicyFilter, error) { 115 | src, err := policy.GenerateRegoFromReader(strings.NewReader(obj.Spec.PPL)) 116 | if err != nil { 117 | return nil, fmt.Errorf("couldn't parse policy: %w", err) 118 | } 119 | 120 | _, err = ast.ParseModule("policy.rego", src) 121 | if err != nil && strings.Contains(err.Error(), "package expected") { 122 | _, err = ast.ParseModule("policy.rego", "package pomerium.policy\n\n"+src) 123 | } 124 | if err != nil { 125 | return nil, fmt.Errorf("internal error: %w", err) 126 | } 127 | return &PolicyFilter{src}, nil 128 | } 129 | 130 | // ApplyToRoute applies this policy filter to a Pomerium route proto. 131 | func (f *PolicyFilter) ApplyToRoute(r *pb.Route) { 132 | r.Policies = append(r.Policies, &pb.Policy{Rego: []string{f.rego}}) 133 | } 134 | -------------------------------------------------------------------------------- /pomerium/gateway/matches.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | gateway_v1 "sigs.k8s.io/gateway-api/apis/v1" 5 | 6 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 7 | ) 8 | 9 | func applyMatch(route *pb.Route, match *gateway_v1.HTTPRouteMatch) (ok bool) { 10 | if len(match.Headers) > 0 || len(match.QueryParams) > 0 || match.Method != nil { 11 | return false // these features are not supported yet 12 | } 13 | applyPathMatch(route, match.Path) 14 | return true 15 | } 16 | 17 | func applyPathMatch(route *pb.Route, match *gateway_v1.HTTPPathMatch) { 18 | if match == nil || match.Type == nil || match.Value == nil { 19 | return 20 | } 21 | 22 | switch *match.Type { 23 | case gateway_v1.PathMatchExact: 24 | route.Path = *match.Value 25 | case gateway_v1.PathMatchPathPrefix: 26 | route.Prefix = *match.Value 27 | case gateway_v1.PathMatchRegularExpression: 28 | route.Regex = *match.Value 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pomerium/gateway/translate.go: -------------------------------------------------------------------------------- 1 | // Package gateway contains logic for converting Gateway API configuration into Pomerium 2 | // configuration. 3 | package gateway 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "net/url" 9 | 10 | "google.golang.org/protobuf/proto" 11 | "sigs.k8s.io/controller-runtime/pkg/log" 12 | 13 | "github.com/pomerium/ingress-controller/model" 14 | "github.com/pomerium/pomerium/config" 15 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 16 | ) 17 | 18 | // TranslateRoutes converts from Gateway-defined routes to Pomerium route configuration protos. 19 | func TranslateRoutes( 20 | ctx context.Context, 21 | gatewayConfig *model.GatewayConfig, 22 | routeConfig *model.GatewayHTTPRouteConfig, 23 | ) []*pb.Route { 24 | // A single HTTPRoute may need to be represented using many Pomerium routes: 25 | // - An HTTPRoute may have multiple hostnames. 26 | // - An HTTPRoute may have multiple HTTPRouteRules. 27 | // - An HTTPRouteRule may have multiple HTTPRouteMatches. 28 | // First we'll expand all HTTPRouteRules into "template" Pomerium routes, and then we'll 29 | // repeat each "template" route once per hostname. 30 | trs := templateRoutes(ctx, gatewayConfig, routeConfig) 31 | 32 | prs := make([]*pb.Route, 0, len(routeConfig.Hostnames)*len(trs)) 33 | for _, h := range routeConfig.Hostnames { 34 | from := (&url.URL{ 35 | Scheme: "https", 36 | Host: string(h), 37 | }).String() 38 | for _, tr := range trs { 39 | r := proto.Clone(tr).(*pb.Route) 40 | r.From = from 41 | 42 | // Skip any routes that fail to validate. 43 | coreRoute, err := config.NewPolicyFromProto(r) 44 | if err != nil || coreRoute.Validate() != nil { 45 | continue 46 | } 47 | 48 | prs = append(prs, r) 49 | } 50 | } 51 | 52 | return prs 53 | } 54 | 55 | // templateRoutes converts an HTTPRoute into zero or more Pomerium routes, ignoring hostname. 56 | func templateRoutes( 57 | ctx context.Context, 58 | gatewayConfig *model.GatewayConfig, 59 | routeConfig *model.GatewayHTTPRouteConfig, 60 | ) []*pb.Route { 61 | logger := log.FromContext(ctx) 62 | 63 | var prs []*pb.Route 64 | 65 | rules := routeConfig.Spec.Rules 66 | for i := range rules { 67 | rule := &rules[i] 68 | pr := &pb.Route{} 69 | 70 | // From the spec (near https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.HTTPRoute): 71 | // "Implementations MUST ignore any port value specified in the HTTP Host header while 72 | // performing a match and (absent of any applicable header modification configuration) MUST 73 | // forward this header unmodified to the backend." 74 | pr.PreserveHostHeader = true 75 | 76 | if err := applyFilters(pr, gatewayConfig, routeConfig, rule.Filters); err != nil { 77 | logger.Error(err, "couldn't apply filter") 78 | pr.Response = &pb.RouteDirectResponse{ 79 | Status: http.StatusInternalServerError, 80 | Body: "invalid filter", 81 | } 82 | } else { 83 | applyBackendRefs(pr, routeConfig, rule.BackendRefs) 84 | } 85 | 86 | if len(rule.Matches) == 0 { 87 | prs = append(prs, pr) 88 | continue 89 | } 90 | 91 | for j := range rule.Matches { 92 | cloned := proto.Clone(pr).(*pb.Route) 93 | if applyMatch(cloned, &rule.Matches[j]) { 94 | prs = append(prs, cloned) 95 | } 96 | } 97 | } 98 | 99 | return prs 100 | } 101 | -------------------------------------------------------------------------------- /pomerium/proto.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "google.golang.org/protobuf/proto" 11 | "google.golang.org/protobuf/reflect/protoreflect" 12 | "google.golang.org/protobuf/types/known/durationpb" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | func unmarshalAnnotations(dst proto.Message, kvs map[string]string) error { 17 | // first convert the map[string]string to a map[string]any via yaml 18 | src := make(map[string]any, len(kvs)) 19 | for k, v := range kvs { 20 | var out any 21 | if err := yaml.Unmarshal([]byte(v), &out); err != nil { 22 | return fmt.Errorf("%s: %w", k, err) 23 | } 24 | src[k] = out 25 | } 26 | 27 | // pre-process the json to handle custom formats 28 | preprocessAnnotationMessage(dst.ProtoReflect().Descriptor(), src) 29 | 30 | // marshal as json so it can be unmarshaled via protojson 31 | data, err := json.Marshal(src) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return protojson.Unmarshal(data, dst) 37 | } 38 | 39 | func preprocessAnnotationMessage(md protoreflect.MessageDescriptor, data any) any { 40 | switch md.FullName() { 41 | case "google.protobuf.Duration": 42 | // convert go duration strings into protojson duration strings 43 | if v, ok := data.(string); ok { 44 | return goDurationStringToProtoJSONDurationString(v) 45 | } 46 | case "pomerium.config.Route.StringList": 47 | if v, ok := data.([]any); ok { 48 | return map[string]any{"values": v} 49 | } 50 | default: 51 | // preprocess all the fields 52 | if v, ok := data.(map[string]any); ok { 53 | fds := md.Fields() 54 | for i := 0; i < fds.Len(); i++ { 55 | fd := fds.Get(i) 56 | name := string(fd.Name()) 57 | vv, ok := v[name] 58 | if ok { 59 | v[name] = preprocessAnnotationField(fd, vv) 60 | } 61 | } 62 | return v 63 | } 64 | } 65 | return data 66 | } 67 | 68 | func preprocessAnnotationField(fd protoreflect.FieldDescriptor, data any) any { 69 | if fd.Enum() != nil && fd.Enum().FullName() == "pomerium.config.BearerTokenFormat" { 70 | if v, ok := data.(string); ok { 71 | switch v { 72 | case "": 73 | return "BEARER_TOKEN_FORMAT_UNKNOWN" 74 | case "default": 75 | return "BEARER_TOKEN_FORMAT_DEFAULT" 76 | case "idp_access_token": 77 | return "BEARER_TOKEN_FORMAT_IDP_ACCESS_TOKEN" 78 | case "idp_identity_token": 79 | return "BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN" 80 | } 81 | } 82 | } 83 | // if this is a repeated field, handle each of the field values separately 84 | if fd.IsList() { 85 | vs, ok := data.([]any) 86 | if ok { 87 | nvs := make([]any, len(vs)) 88 | for i, v := range vs { 89 | nvs[i] = preprocessAnnotationFieldValue(fd, v) 90 | } 91 | return nvs 92 | } 93 | } 94 | 95 | return preprocessAnnotationFieldValue(fd, data) 96 | } 97 | 98 | func preprocessAnnotationFieldValue(fd protoreflect.FieldDescriptor, data any) any { 99 | // convert map[string]any -> map[string]string 100 | if fd.IsMap() && fd.MapKey().Kind() == protoreflect.StringKind && fd.MapValue().Kind() == protoreflect.StringKind { 101 | if v, ok := data.(map[string]any); ok { 102 | m := make(map[string]string, len(v)) 103 | for k, vv := range v { 104 | m[k] = fmt.Sprint(vv) 105 | } 106 | return m 107 | } 108 | } 109 | 110 | switch fd.Kind() { 111 | case protoreflect.MessageKind: 112 | return preprocessAnnotationMessage(fd.Message(), data) 113 | } 114 | 115 | return data 116 | } 117 | 118 | func goDurationStringToProtoJSONDurationString(in string) string { 119 | dur, err := time.ParseDuration(in) 120 | if err != nil { 121 | return in 122 | } 123 | 124 | bs, err := protojson.Marshal(durationpb.New(dur)) 125 | if err != nil { 126 | return in 127 | } 128 | 129 | str := strings.Trim(string(bs), `"`) 130 | return str 131 | } 132 | -------------------------------------------------------------------------------- /pomerium/reconcile.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/types" 7 | 8 | "github.com/pomerium/ingress-controller/model" 9 | ) 10 | 11 | // IngressReconciler updates pomerium configuration based on provided network resources 12 | // it is not expected to be thread safe 13 | type IngressReconciler interface { 14 | // Upsert should update or create the pomerium routes corresponding to this ingress 15 | Upsert(ctx context.Context, ic *model.IngressConfig) (changes bool, err error) 16 | // Set configuration to match provided ingresses and shared config settings 17 | Set(ctx context.Context, ics []*model.IngressConfig) (changes bool, err error) 18 | // Delete should delete pomerium routes corresponding to this ingress name 19 | Delete(ctx context.Context, namespacedName types.NamespacedName) (changes bool, err error) 20 | } 21 | 22 | // GatewayReconciler updates Pomerium configuration based on Gateway-defined resources. 23 | type GatewayReconciler interface { 24 | // GatewaySetConfig updates the entire Gateway-defined route configuration. 25 | SetGatewayConfig(ctx context.Context, config *model.GatewayConfig) (changes bool, err error) 26 | } 27 | 28 | // ConfigReconciler only updates global parameters and does not deal with individual routes 29 | type ConfigReconciler interface { 30 | // SetConfig updates just the shared config settings 31 | SetConfig(ctx context.Context, cfg *model.Config) (changes bool, err error) 32 | } 33 | -------------------------------------------------------------------------------- /pomerium/route_list.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | 8 | "k8s.io/apimachinery/pkg/types" 9 | 10 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 11 | ) 12 | 13 | type routeID struct { 14 | Name string `json:"n"` 15 | Namespace string `json:"ns"` 16 | Host string `json:"h"` 17 | Path string `json:"p"` 18 | } 19 | 20 | func (r *routeID) Marshal() (string, error) { 21 | data, err := json.Marshal(r) 22 | if err != nil { 23 | return "", err 24 | } 25 | return string(data), nil 26 | } 27 | 28 | func (r *routeID) Unmarshal(txt string) error { 29 | return json.Unmarshal([]byte(txt), r) 30 | } 31 | 32 | type routeList []*pb.Route 33 | type routeMap map[routeID]*pb.Route 34 | 35 | func (routes routeList) Sort() { sort.Sort(routes) } 36 | func (routes routeList) Len() int { return len(routes) } 37 | func (routes routeList) Swap(i, j int) { routes[i], routes[j] = routes[j], routes[i] } 38 | 39 | // Less reports whether the element with 40 | // index i should sort before the element with index j. 41 | // as envoy parses routes as presented, we should presents routes with longer paths first 42 | // exact Path always takes priority over Prefix matching 43 | func (routes routeList) Less(i, j int) bool { 44 | // from ASC 45 | iFrom, jFrom := routes[i].GetFrom(), routes[j].GetFrom() 46 | switch { 47 | case iFrom < jFrom: 48 | return true 49 | case iFrom > jFrom: 50 | return false 51 | } 52 | 53 | // path DESC 54 | iPath, jPath := routes[i].GetPath(), routes[j].GetPath() 55 | switch { 56 | case iPath < jPath: 57 | return false 58 | case iPath > jPath: 59 | return true 60 | } 61 | 62 | // regex DESC 63 | iRegex, jRegex := routes[i].GetRegex(), routes[j].GetRegex() 64 | switch { 65 | case iRegex < jRegex: 66 | return false 67 | case iRegex > jRegex: 68 | return true 69 | } 70 | 71 | // prefix DESC 72 | iPrefix, jPrefix := routes[i].GetPrefix(), routes[j].GetPrefix() 73 | switch { 74 | case iPrefix < jPrefix: 75 | return false 76 | case iPrefix > jPrefix: 77 | return true 78 | } 79 | 80 | // finally, by id 81 | iID, jID := routes[i].GetId(), routes[j].GetId() 82 | switch { 83 | case iID < jID: 84 | return true 85 | case iID > jID: 86 | return false 87 | } 88 | 89 | return false 90 | } 91 | 92 | func (routes routeList) toMap() (routeMap, error) { 93 | m := make(routeMap, len(routes)) 94 | for _, r := range routes { 95 | var key routeID 96 | if err := key.Unmarshal(r.Id); err != nil { 97 | return nil, fmt.Errorf("cannot decode route id %s: %w", r.Id, err) 98 | } 99 | if _, exists := m[key]; exists { 100 | return nil, fmt.Errorf("duplicate route %+v", key) 101 | } 102 | m[key] = r 103 | } 104 | return m, nil 105 | } 106 | 107 | func (rm routeMap) removeName(name types.NamespacedName) { 108 | for k := range rm { 109 | if k.Name == name.Name && k.Namespace == name.Namespace { 110 | delete(rm, k) 111 | } 112 | } 113 | } 114 | 115 | func (rm routeMap) toList() routeList { 116 | routes := make(routeList, 0, len(rm)) 117 | for _, r := range rm { 118 | routes = append(routes, r) 119 | } 120 | sort.Sort(routes) 121 | return routes 122 | } 123 | 124 | func (rm routeMap) merge(src routeMap) { 125 | for id, r := range src { 126 | rm[id] = r 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /pomerium/routes.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/types" 8 | 9 | "github.com/pomerium/ingress-controller/model" 10 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 11 | ) 12 | 13 | func mergeRoutes(dst *pb.Config, src routeList, name types.NamespacedName) error { 14 | srcMap, err := src.toMap() 15 | if err != nil { 16 | return fmt.Errorf("indexing new routes: %w", err) 17 | } 18 | dstMap, err := routeList(dst.Routes).toMap() 19 | if err != nil { 20 | return fmt.Errorf("indexing current config routes: %w", err) 21 | } 22 | // remove any existing routes of the ingress we are merging 23 | dstMap.removeName(name) 24 | dstMap.merge(srcMap) 25 | dst.Routes = dstMap.toList() 26 | 27 | return nil 28 | } 29 | 30 | func upsertRoutes(ctx context.Context, cfg *pb.Config, ic *model.IngressConfig) error { 31 | ingRoutes, err := ingressToRoutes(ctx, ic) 32 | if err != nil { 33 | return fmt.Errorf("parsing ingress: %w", err) 34 | } 35 | return mergeRoutes(cfg, ingRoutes, types.NamespacedName{Name: ic.Ingress.Name, Namespace: ic.Ingress.Namespace}) 36 | } 37 | 38 | func deleteRoutes(cfg *pb.Config, namespacedName types.NamespacedName) error { 39 | rm, err := routeList(cfg.Routes).toMap() 40 | if err != nil { 41 | return err 42 | } 43 | rm.removeName(namespacedName) 44 | cfg.Routes = rm.toList() 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pomerium/validate.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "golang.org/x/net/nettest" 8 | 9 | "github.com/pomerium/pomerium/config" 10 | "github.com/pomerium/pomerium/config/envoyconfig" 11 | "github.com/pomerium/pomerium/config/envoyconfig/filemgr" 12 | "github.com/pomerium/pomerium/pkg/cryptutil" 13 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 14 | 15 | "github.com/pomerium/ingress-controller/pomerium/envoy" 16 | ) 17 | 18 | // validate validates pomerium config. 19 | func validate(ctx context.Context, cfg *pb.Config, id string) error { 20 | options := config.NewDefaultOptions() 21 | options.ApplySettings(ctx, cryptutil.NewCertificatesIndex(), cfg.GetSettings()) 22 | options.InsecureServer = true 23 | 24 | for _, r := range cfg.GetRoutes() { 25 | p, err := config.NewPolicyFromProto(r) 26 | if err != nil { 27 | return err 28 | } 29 | err = p.Validate() 30 | if err != nil { 31 | return err 32 | } 33 | options.Policies = append(options.Policies, *p) 34 | } 35 | 36 | err := options.Validate() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | pCfg := &config.Config{Options: options, OutboundPort: "8002"} 42 | 43 | builder := envoyconfig.New("127.0.0.1:8000", "127.0.0.1:8001", "127.0.0.1:8003", filemgr.NewManager(), nil, nettest.SupportsIPv6()) 44 | bootstrap, err := builder.BuildBootstrap(ctx, pCfg, true) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | res, err := envoy.Validate(ctx, bootstrap, id) 50 | if err != nil { 51 | return err 52 | } 53 | if !res.Valid { 54 | return errors.New(res.Message) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /scripts/check-docker-images: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | set -euo pipefail 3 | 4 | inspect-manifest() { 5 | local _image 6 | _image="${1?"image is required"}" 7 | 8 | local _temp_dir 9 | _temp_dir="${TMPDIR-/tmp}" 10 | local _image_hash 11 | _image_hash="$(echo -n "$_image" | shasum | cut -f1 -d' ')" 12 | local _temp_file 13 | _temp_file="${_temp_dir}/check-docker-image-${_image_hash}.json" 14 | 15 | if [ ! -f "$_temp_file" ]; then 16 | docker buildx imagetools inspect \ 17 | --format='{{json .}}' \ 18 | "$_image" >"$_temp_file" 19 | fi 20 | 21 | cat "$_temp_file" 22 | } 23 | 24 | check-image() { 25 | local _image 26 | _image="${1?"image is required"}" 27 | 28 | echo "checking image=$_image" 29 | 30 | local _manifest 31 | _manifest="$(inspect-manifest "$_image")" 32 | 33 | local _has_arm64 34 | _has_arm64="$(echo "$_manifest" | jq ' 35 | .manifest.manifests 36 | | map(select(.platform.architecture == "arm64" and .platform.os == "linux")) 37 | | length >= 1 38 | ')" 39 | 40 | if [[ "$_has_arm64" != "true" ]]; then 41 | echo "- missing ARM64 in $_manifest" 42 | exit 1 43 | fi 44 | 45 | local _has_amd64 46 | _has_amd64="$(echo "$_manifest" | jq ' 47 | .manifest.manifests 48 | | map(select(.platform.architecture == "amd64" and .platform.os == "linux")) 49 | | length >= 1 50 | ')" 51 | 52 | if [[ "$_has_amd64" != "true" ]]; then 53 | echo "- missing AMD64 in $_manifest" 54 | exit 1 55 | fi 56 | } 57 | 58 | check-dockerfile() { 59 | local _file 60 | _file="${1?"file is required"}" 61 | 62 | echo "checking dockerfile=$_file" 63 | 64 | while IFS= read -r _image; do 65 | check-image "$_image" 66 | done < <(sed -n -r -e 's/^FROM ([^:]*)(:[^@]*)(@sha256[^ ]*).*$/\1\2\3/p' "$_file") 67 | } 68 | 69 | check-directory() { 70 | local _directory 71 | _directory="${1?"directory is required"}" 72 | 73 | echo "checking directory=$_directory" 74 | 75 | local _file 76 | while IFS= read -r -d '' _file; do 77 | check-dockerfile "$_file" 78 | done < <(find "$_directory" -name "*Dockerfile*" -print0) 79 | } 80 | 81 | main() { 82 | local _project_root 83 | _project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/.." 84 | 85 | check-directory "$_project_root" 86 | } 87 | 88 | main 89 | -------------------------------------------------------------------------------- /scripts/check-image-tag.sh: -------------------------------------------------------------------------------- 1 | # checks that image tag matches the argument 2 | 3 | set -e 4 | want=$1 5 | if [[ -z "${want}" ]]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | img=$(yq eval '.spec.template.spec.containers[0].image' config/pomerium/deployment/image.yaml) 11 | tag=${img#pomerium/ingress-controller:} 12 | if [[ "${tag}" != "${want}" ]]; then 13 | echo "Image tag mismatch: ${tag} != ${want}" 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /scripts/open-docs-pull-request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | source_path="reference.md" 5 | destination_repo="pomerium/documentation" 6 | destination_path="content/docs/deploy/k8s" 7 | destination_base_branch="main" 8 | destination_head_branch="update-k8s-reference-$GITHUB_SHA" 9 | 10 | clone_dir=$(mktemp -d) 11 | 12 | export GITHUB_TOKEN=$API_TOKEN_GITHUB 13 | 14 | echo "Cloning destination git repository" 15 | git clone --depth 1 \ 16 | "https://$API_TOKEN_GITHUB@github.com/$destination_repo.git" "$clone_dir" 17 | 18 | echo "Copying contents to git repo" 19 | cp -R "$source_path" "$clone_dir/$destination_path" 20 | cd "$clone_dir" 21 | yarn && yarn prettier --write "$destination_path/$source_path" 22 | git checkout -b "$destination_head_branch" 23 | 24 | if [ -z "$(git status -z)" ]; then 25 | echo "No changes detected" 26 | exit 27 | fi 28 | 29 | echo "Adding git commit" 30 | git config user.email "$USER_EMAIL" 31 | git config user.name "$USER_NAME" 32 | git add . 33 | message="Update $destination_path from $GITHUB_REPOSITORY@$GITHUB_SHA." 34 | git commit --message "$message" 35 | 36 | echo "Pushing git commit" 37 | git push -u origin HEAD:$destination_head_branch 38 | 39 | echo "Creating a pull request" 40 | gh pr create --title $destination_head_branch --body "$message" \ 41 | --base $destination_base_branch --head $destination_head_branch 42 | -------------------------------------------------------------------------------- /util/bin.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "context" 4 | 5 | type key[T any] struct{} 6 | 7 | type bin[T any] struct { 8 | entries []T 9 | } 10 | 11 | // WithBin enables collector of objects that's stored in context 12 | // that may be used to collect i.e. some warnings that do not cause errors 13 | // or maybe document some defaults that were applied 14 | func WithBin[T any](ctx context.Context) context.Context { 15 | k := key[T]{} 16 | _, ok := ctx.Value(k).(*bin[T]) 17 | if ok { 18 | return ctx 19 | } 20 | return context.WithValue(ctx, k, new(bin[T])) 21 | } 22 | 23 | // Add attaches an entry to the collector 24 | func Add[T any](ctx context.Context, entries ...T) { 25 | collector, ok := ctx.Value(key[T]{}).(*bin[T]) 26 | if !ok { 27 | return 28 | } 29 | collector.entries = append(collector.entries, entries...) 30 | } 31 | 32 | // Get returns all entries attached to the collector 33 | func Get[T any](ctx context.Context) []T { 34 | collector, ok := ctx.Value(key[T]{}).(*bin[T]) 35 | if !ok { 36 | return nil 37 | } 38 | return collector.entries 39 | } 40 | -------------------------------------------------------------------------------- /util/bin_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/pomerium/ingress-controller/util" 10 | ) 11 | 12 | type testType string 13 | 14 | func TestBin(t *testing.T) { 15 | ctx := util.WithBin[testType](context.Background()) 16 | util.Add(ctx, testType("test")) 17 | require.Equal(t, []testType{"test"}, util.Get[testType](ctx)) 18 | } 19 | -------------------------------------------------------------------------------- /util/generic/builder.go: -------------------------------------------------------------------------------- 1 | package generic 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | "sigs.k8s.io/controller-runtime/pkg/event" 6 | "sigs.k8s.io/controller-runtime/pkg/predicate" 7 | ) 8 | 9 | // NewPredicateFuncs is a wrapper around predicate.NewTypedPredicateFuncs[T] 10 | // that converts the typed predicate functions back to untyped variants, suitable 11 | // for use in the current controller-runtime API. 12 | // 13 | // When controller-runtime is updated to use generic builders, this function 14 | // can be removed. See https://github.com/kubernetes-sigs/controller-runtime/pull/2784 15 | func NewPredicateFuncs[T client.Object](f func(T) bool) predicate.TypedFuncs[client.Object] { 16 | return asUntypedPredicateFuncs(predicate.NewTypedPredicateFuncs(f)) 17 | } 18 | 19 | func asUntypedPredicateFuncs[T client.Object](p predicate.TypedFuncs[T]) predicate.TypedFuncs[client.Object] { 20 | return predicate.TypedFuncs[client.Object]{ 21 | CreateFunc: func(e event.TypedCreateEvent[client.Object]) bool { 22 | return p.CreateFunc(event.TypedCreateEvent[T]{ 23 | Object: e.Object.(T), 24 | }) 25 | }, 26 | DeleteFunc: func(e event.TypedDeleteEvent[client.Object]) bool { 27 | return p.DeleteFunc(event.TypedDeleteEvent[T]{ 28 | Object: e.Object.(T), 29 | DeleteStateUnknown: e.DeleteStateUnknown, 30 | }) 31 | }, 32 | UpdateFunc: func(e event.TypedUpdateEvent[client.Object]) bool { 33 | return p.UpdateFunc(event.TypedUpdateEvent[T]{ 34 | ObjectOld: e.ObjectOld.(T), 35 | ObjectNew: e.ObjectNew.(T), 36 | }) 37 | }, 38 | GenericFunc: func(e event.TypedGenericEvent[client.Object]) bool { 39 | return p.GenericFunc(event.TypedGenericEvent[T]{ 40 | Object: e.Object.(T), 41 | }) 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /util/generic/doc.go: -------------------------------------------------------------------------------- 1 | // Package generic contains helper functions useful for working with the generic 2 | // controller-runtime APIs. 3 | package generic 4 | -------------------------------------------------------------------------------- /util/generic/gvk.go: -------------------------------------------------------------------------------- 1 | package generic 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 11 | ) 12 | 13 | // GVKForType returns the GroupVersionKind for a given type T registered in the scheme. 14 | // It panics if the type is not registered or there is more than one GVK for the type. 15 | func GVKForType[T client.Object](scheme *runtime.Scheme) schema.GroupVersionKind { 16 | t := reflect.New(reflect.TypeFor[T]().Elem()).Interface().(T) 17 | gvk, err := apiutil.GVKForObject(t, scheme) 18 | if err != nil { 19 | panic(fmt.Errorf("bug: %w", err)) 20 | } 21 | return gvk 22 | } 23 | -------------------------------------------------------------------------------- /util/merge_map.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "fmt" 4 | 5 | // MergeMaps is used to merge configmap and secret values 6 | func MergeMaps(first map[string]string, second map[string][]byte) (map[string]string, error) { 7 | dst := make(map[string]string) 8 | for key, val := range first { 9 | dst[key] = val 10 | } 11 | for key, data := range second { 12 | if _, there := first[key]; there { 13 | return nil, fmt.Errorf("secret contains key %s that was already specified by a non-secret rule", key) 14 | } 15 | dst[key] = string(data) 16 | } 17 | return dst, nil 18 | } 19 | -------------------------------------------------------------------------------- /util/merge_map_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/pomerium/ingress-controller/util" 9 | ) 10 | 11 | func TestMergeMap(t *testing.T) { 12 | for _, tc := range []struct { 13 | name string 14 | src map[string][]byte 15 | dst map[string]string 16 | expect map[string]string 17 | expectError bool 18 | }{ 19 | {name: "nothing", src: nil, dst: nil, expect: map[string]string{}, expectError: false}, 20 | {name: "key overlap", src: map[string][]byte{ 21 | "k1": []byte("v1"), 22 | }, dst: map[string]string{ 23 | "k1": "v1.1", 24 | "k2": "v2", 25 | }, expect: nil, expectError: true}, 26 | {name: "no overlap", src: map[string][]byte{ 27 | "k1": []byte("v1"), 28 | }, dst: map[string]string{ 29 | "k2": "v2", 30 | }, expect: map[string]string{ 31 | "k1": "v1", 32 | "k2": "v2", 33 | }, expectError: false}, 34 | } { 35 | got, err := util.MergeMaps(tc.dst, tc.src) 36 | if tc.expectError { 37 | assert.Error(t, err, tc.name) 38 | continue 39 | } 40 | if assert.NoError(t, err, tc.name) { 41 | assert.Equal(t, tc.expect, got) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /util/namespaced_name.go: -------------------------------------------------------------------------------- 1 | // Package util contains misc utils 2 | package util 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | ) 12 | 13 | var ( 14 | // ErrInvalidNamespacedNameFormat namespaced name format error 15 | ErrInvalidNamespacedNameFormat = errors.New("invalid format, expect name or namespace/name") 16 | // ErrNamespaceExpected indicates that a namespace must be provided 17 | ErrNamespaceExpected = errors.New("missing namespace for resource") 18 | // ErrEmptyName indicates the resource must be non-empty 19 | ErrEmptyName = errors.New("resource name cannot be blank") 20 | ) 21 | 22 | // NamespacedNameOption customizes namespaced name parsing 23 | type NamespacedNameOption func(name *types.NamespacedName) error 24 | 25 | // WithNamespaceExpected will set namespace to provided default, if missing 26 | func WithNamespaceExpected() NamespacedNameOption { 27 | return func(name *types.NamespacedName) error { 28 | if name.Namespace == "" { 29 | return ErrNamespaceExpected 30 | } 31 | return nil 32 | } 33 | } 34 | 35 | // WithDefaultNamespace will set namespace to provided default, if missing 36 | func WithDefaultNamespace(namespace string) NamespacedNameOption { 37 | return func(name *types.NamespacedName) error { 38 | if namespace == "" { 39 | return ErrNamespaceExpected 40 | } 41 | 42 | if name.Namespace == "" { 43 | name.Namespace = namespace 44 | } 45 | return nil 46 | } 47 | } 48 | 49 | // WithMustNamespace enforces the namespace to match provided one 50 | func WithMustNamespace(namespace string) NamespacedNameOption { 51 | return func(name *types.NamespacedName) error { 52 | if namespace == "" { 53 | return ErrNamespaceExpected 54 | } 55 | 56 | if name.Namespace == "" { 57 | name.Namespace = namespace 58 | } else if name.Namespace != namespace { 59 | return fmt.Errorf("expected namespace %s, got %s", namespace, name.Namespace) 60 | } 61 | return nil 62 | } 63 | } 64 | 65 | // WithClusterScope ensures the name is not namespaced 66 | func WithClusterScope() NamespacedNameOption { 67 | return func(name *types.NamespacedName) error { 68 | if name.Namespace != "" { 69 | return fmt.Errorf("expected cluster-scoped name") 70 | } 71 | return nil 72 | } 73 | } 74 | 75 | // ParseNamespacedName parses "namespace/name" or "name" format 76 | func ParseNamespacedName(name string, options ...NamespacedNameOption) (*types.NamespacedName, error) { 77 | if len(options) > 1 { 78 | return nil, errors.New("at most one option may be supplied") 79 | } 80 | 81 | if len(options) == 0 { 82 | options = []NamespacedNameOption{WithNamespaceExpected()} 83 | } 84 | 85 | if name == "" { 86 | return nil, ErrEmptyName 87 | } 88 | 89 | parts := strings.Split(name, "/") 90 | var dst types.NamespacedName 91 | switch len(parts) { 92 | case 1: 93 | dst.Name = parts[0] 94 | case 2: 95 | dst.Namespace = parts[0] 96 | dst.Name = parts[1] 97 | default: 98 | return nil, ErrInvalidNamespacedNameFormat 99 | } 100 | 101 | for _, opt := range options { 102 | if err := opt(&dst); err != nil { 103 | return nil, err 104 | } 105 | } 106 | 107 | if dst.Name == "" { 108 | return nil, ErrInvalidNamespacedNameFormat 109 | } 110 | 111 | return &dst, nil 112 | } 113 | 114 | // GetNamespacedName a convenience method to return types.NamespacedName for an object 115 | func GetNamespacedName(obj metav1.Object) types.NamespacedName { 116 | return types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()} 117 | } 118 | -------------------------------------------------------------------------------- /util/namespaced_name_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "k8s.io/apimachinery/pkg/types" 9 | 10 | "github.com/pomerium/ingress-controller/util" 11 | ) 12 | 13 | func TestParseNamespacedName(t *testing.T) { 14 | for _, tc := range []struct { 15 | in string 16 | opts []util.NamespacedNameOption 17 | want *types.NamespacedName 18 | errCheck func(require.TestingT, error, ...interface{}) 19 | }{{ 20 | "no_namespace", 21 | nil, 22 | nil, 23 | require.Error, 24 | }, { 25 | "", 26 | nil, 27 | nil, 28 | require.Error, 29 | }, { 30 | "empty_default_namespace", 31 | []util.NamespacedNameOption{util.WithDefaultNamespace("")}, 32 | nil, 33 | require.Error, 34 | }, { 35 | "empty_required_namespace", 36 | []util.NamespacedNameOption{util.WithMustNamespace("")}, 37 | nil, 38 | require.Error, 39 | }, { 40 | "with_must_namespace", 41 | []util.NamespacedNameOption{util.WithMustNamespace("default")}, 42 | &types.NamespacedName{Namespace: "default", Name: "with_must_namespace"}, 43 | require.NoError, 44 | }, { 45 | "empty_must_namespace", 46 | []util.NamespacedNameOption{util.WithMustNamespace("")}, 47 | nil, 48 | require.Error, 49 | }, { 50 | "with_default_namespace", 51 | []util.NamespacedNameOption{util.WithDefaultNamespace("default")}, 52 | &types.NamespacedName{Namespace: "default", Name: "with_default_namespace"}, 53 | require.NoError, 54 | }, { 55 | "pomerium/name", 56 | []util.NamespacedNameOption{util.WithDefaultNamespace("default")}, 57 | &types.NamespacedName{Namespace: "pomerium", Name: "name"}, 58 | require.NoError, 59 | }, { 60 | "cluster-scoped", 61 | []util.NamespacedNameOption{util.WithClusterScope()}, 62 | &types.NamespacedName{Name: "cluster-scoped"}, 63 | require.NoError, 64 | }, { 65 | "pomerium/name", 66 | []util.NamespacedNameOption{util.WithClusterScope()}, 67 | nil, 68 | require.Error, 69 | }, { 70 | "wrong/format/here", 71 | nil, 72 | nil, 73 | require.Error, 74 | }, { 75 | "enforced_namespace/name", 76 | []util.NamespacedNameOption{util.WithMustNamespace("pomerium")}, 77 | nil, 78 | require.Error, 79 | }} { 80 | t.Run(tc.in, func(t *testing.T) { 81 | got, err := util.ParseNamespacedName(tc.in, tc.opts...) 82 | tc.errCheck(t, err, "error check") 83 | assert.Equal(t, tc.want, got) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /util/restart.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "golang.org/x/sync/errgroup" 9 | "sigs.k8s.io/controller-runtime/pkg/log" 10 | ) 11 | 12 | // Config is a configuration abstraction 13 | type Config[T any] interface { 14 | Clone() T 15 | } 16 | 17 | // RestartOnConfigChange allows to run a task that should be restarted when config changes. 18 | type RestartOnConfigChange[T Config[T]] interface { 19 | // OnConfigUpdated may be called to provide updated configuration versions 20 | OnConfigUpdated(context.Context, T) 21 | // Run runs the task, restarting it when equal() returns false, by canceling task's execution context 22 | // if the task cannot complete within the given timeout threshold, Run would return an error 23 | // ensuring there are no unaccounted tasks left 24 | Run(ctx context.Context, 25 | equal func(prev, next T) bool, 26 | fn func(context.Context, T) error, 27 | shutdownTimeout time.Duration, 28 | ) error 29 | } 30 | 31 | // NewRestartOnChange create a new instance 32 | func NewRestartOnChange[T Config[T]]() RestartOnConfigChange[T] { 33 | return &restartOnChange[T]{ 34 | updates: make(chan T), 35 | } 36 | } 37 | 38 | type restartOnChange[T Config[T]] struct { 39 | updates chan T 40 | } 41 | 42 | func (r *restartOnChange[T]) OnConfigUpdated(ctx context.Context, cfg T) { 43 | select { 44 | case <-ctx.Done(): 45 | log.FromContext(ctx).Error(ctx.Err(), "failed to push config update into a queue") 46 | case r.updates <- cfg.Clone(): 47 | } 48 | } 49 | 50 | func (r *restartOnChange[T]) Run(ctx context.Context, 51 | equal func(prev, next T) bool, 52 | fn func(ctx context.Context, cfg T) error, 53 | shutdownTimeout time.Duration, 54 | ) error { 55 | updates := make(chan T) 56 | defer close(updates) 57 | 58 | eg, ctx := errgroup.WithContext(ctx) 59 | eg.Go(func() error { filterChanges(ctx, updates, r.updates, equal); return nil }) 60 | eg.Go(func() error { return runTasks(ctx, updates, fn, shutdownTimeout) }) 61 | 62 | return eg.Wait() 63 | } 64 | 65 | func runTasks[T any](ctx context.Context, 66 | updates chan T, 67 | fn func(context.Context, T) error, 68 | shutdownTimeout time.Duration, 69 | ) error { 70 | var cancel context.CancelFunc = func() {} 71 | defer cancel() 72 | 73 | logger := log.FromContext(ctx).V(1) 74 | logger.Info("starting task loop") 75 | var done chan error 76 | for { 77 | select { 78 | case <-ctx.Done(): 79 | logger.Info("context canceled, quit") 80 | return nil 81 | case cfg := <-updates: 82 | cancel() 83 | logger.Info("canceling and waiting for previous task to finish...") 84 | if err := waitCompletion(ctx, done, shutdownTimeout); err != nil { 85 | logger.Error(err, "waiting for task to finish") 86 | return fmt.Errorf("waiting for task completion: %w", err) 87 | } 88 | logger.Info("restarting new task", "config", cfg) 89 | cancel, done = goRunTask(ctx, cfg, fn) 90 | case err := <-done: 91 | logger.Error(err, "task quit unexpectedly") 92 | return fmt.Errorf("task quit: %w", err) 93 | } 94 | } 95 | } 96 | 97 | func waitCompletion(ctx context.Context, done chan error, timeout time.Duration) error { 98 | if done == nil { 99 | return nil 100 | } 101 | 102 | ctx, cancel := context.WithTimeout(ctx, timeout) 103 | defer cancel() 104 | 105 | select { 106 | case <-ctx.Done(): 107 | return ctx.Err() 108 | case <-done: 109 | return nil 110 | } 111 | } 112 | 113 | func goRunTask[T any](ctx context.Context, cfg T, fn func(context.Context, T) error) (context.CancelFunc, chan error) { 114 | ctx, cancel := context.WithCancel(ctx) 115 | 116 | done := make(chan error) 117 | go func() { 118 | done <- fn(ctx, cfg) 119 | close(done) 120 | }() 121 | 122 | return cancel, done 123 | } 124 | 125 | func filterChanges[T any](ctx context.Context, dst, src chan T, equal func(prev, next T) bool) { 126 | var cfg T 127 | for { 128 | select { 129 | case <-ctx.Done(): 130 | return 131 | case next, ok := <-src: 132 | if !ok { 133 | return 134 | } 135 | if equal(cfg, next) { 136 | continue 137 | } 138 | cfg = next 139 | select { 140 | case <-ctx.Done(): 141 | return 142 | case dst <- cfg: 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /util/restart_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "golang.org/x/sync/errgroup" 13 | ) 14 | 15 | type testConfig string 16 | 17 | func (t testConfig) Clone() testConfig { 18 | return t 19 | } 20 | 21 | func TestFilter(t *testing.T) { 22 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 23 | defer cancel() 24 | 25 | src := make(chan testConfig) 26 | go func() { 27 | for _, txt := range []testConfig{"a", "a", "b", "c", "c", "d"} { 28 | src <- txt 29 | } 30 | close(src) 31 | }() 32 | dst := make(chan testConfig) 33 | go func() { 34 | filterChanges(ctx, dst, src, func(prev, next testConfig) bool { return prev == next }) 35 | close(dst) 36 | }() 37 | 38 | var res []testConfig 39 | for txt := range dst { 40 | res = append(res, txt) 41 | } 42 | 43 | assert.Equal(t, []testConfig{"a", "b", "c", "d"}, res) 44 | } 45 | 46 | func TestRunTasks(t *testing.T) { 47 | for _, tc := range []struct { 48 | name string 49 | jobs, want []testConfig 50 | check func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool 51 | }{ 52 | { 53 | "work", 54 | []testConfig{"work-1"}, 55 | []testConfig{"work-1"}, 56 | assert.NoError, 57 | }, 58 | { 59 | "work duplicates", 60 | []testConfig{"work-1", "work-1"}, 61 | []testConfig{"work-1"}, 62 | assert.NoError, 63 | }, 64 | { 65 | "work repeated", 66 | []testConfig{"work-1", "work-2", "work-1"}, 67 | []testConfig{"work-1", "work-2", "work-1"}, 68 | assert.NoError, 69 | }, 70 | { 71 | "work skip equal", 72 | []testConfig{"work-1", "work-1", "work-1", "work-2"}, 73 | []testConfig{"work-1", "work-2"}, 74 | assert.NoError, 75 | }, 76 | { 77 | "error", 78 | []testConfig{"work-1", "error-1"}, 79 | []testConfig{"work-1", "error-1"}, 80 | assert.Error, 81 | }, 82 | { 83 | "long shutdown within limits", 84 | []testConfig{"work-1", "wait-1"}, 85 | []testConfig{"work-1", "wait-1"}, 86 | assert.NoError, 87 | }, 88 | { 89 | "shut down too long", 90 | []testConfig{"work-1", "lock-1", "work-2"}, 91 | []testConfig{"work-1", "lock-1"}, 92 | assert.Error, 93 | }, 94 | } { 95 | t.Run(tc.name, func(t *testing.T) { 96 | got, err := testRunTasks(tc.jobs) 97 | t.Log(tc.jobs, "=>", got, err) 98 | if tc.check(t, err) { 99 | assert.Equal(t, tc.want, got) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func testRunTasks(jobs []testConfig) ([]testConfig, error) { 106 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 107 | defer cancel() 108 | 109 | var got []testConfig 110 | 111 | taskTimeout := time.Second 112 | 113 | eg, ctx := errgroup.WithContext(ctx) 114 | r := NewRestartOnChange[testConfig]() 115 | eg.Go(func() error { 116 | return r.Run(ctx, 117 | func(prev, next testConfig) bool { return prev == next }, 118 | func(ctx context.Context, tc testConfig) error { 119 | got = append(got, tc) 120 | 121 | if strings.HasPrefix(string(tc), "work-") { 122 | <-ctx.Done() 123 | return fmt.Errorf("%s: %w", tc, ctx.Err()) 124 | } else if strings.HasPrefix(string(tc), "wait-") { 125 | <-ctx.Done() 126 | time.Sleep(taskTimeout / 2) 127 | return fmt.Errorf("%s: %w", tc, ctx.Err()) 128 | } else if strings.HasPrefix(string(tc), "lock-") { 129 | <-ctx.Done() 130 | time.Sleep(taskTimeout * 2) 131 | return fmt.Errorf("%s: %w", tc, ctx.Err()) 132 | } else if strings.HasPrefix(string(tc), "error-") { 133 | return errors.New(string(tc)) 134 | } 135 | return fmt.Errorf("unexpected %s", tc) 136 | }, 137 | time.Second) 138 | }) 139 | eg.Go(func() error { 140 | for _, tc := range jobs { 141 | r.OnConfigUpdated(ctx, tc) 142 | select { 143 | case <-ctx.Done(): 144 | return fmt.Errorf("waiting for task results: %w", ctx.Err()) 145 | case <-time.After(time.Millisecond * 200): 146 | } 147 | } 148 | cancel() 149 | return nil 150 | }) 151 | 152 | return got, eg.Wait() 153 | } 154 | -------------------------------------------------------------------------------- /util/secrets.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | 10 | "github.com/pomerium/pomerium/pkg/cryptutil" 11 | ) 12 | 13 | // NewBootstrapSecrets generate secrets for pomerium bootstrap 14 | func NewBootstrapSecrets(name types.NamespacedName) (*corev1.Secret, error) { 15 | key, err := cryptutil.NewSigningKey() 16 | if err != nil { 17 | return nil, fmt.Errorf("gen key: %w", err) 18 | } 19 | signingKey, err := cryptutil.EncodePrivateKey(key) 20 | if err != nil { 21 | return nil, fmt.Errorf("pem: %w", err) 22 | } 23 | 24 | return &corev1.Secret{ 25 | ObjectMeta: metav1.ObjectMeta{Name: name.Name, Namespace: name.Namespace}, 26 | Data: map[string][]byte{ 27 | "shared_secret": cryptutil.NewKey(), 28 | "cookie_secret": cryptutil.NewKey(), 29 | "signing_key": signingKey, 30 | }, 31 | Type: corev1.SecretTypeOpaque, 32 | }, nil 33 | } 34 | --------------------------------------------------------------------------------