├── config ├── .gitignore ├── pomerium │ ├── namespace.yaml │ ├── rbac │ │ ├── service_account.yaml │ │ ├── kustomization.yaml │ │ ├── role_binding.yaml │ │ └── role.yaml │ ├── service │ │ ├── kustomization.yaml │ │ ├── metrics.yaml │ │ └── proxy.yaml │ ├── ingressclass.yaml │ ├── kustomization.yaml │ └── deployment │ │ ├── image.yaml │ │ ├── kustomization.yaml │ │ ├── resources.yaml │ │ ├── no-root.yaml │ │ ├── base.yaml │ │ ├── ports.yaml │ │ ├── readonly-root-fs.yaml │ │ ├── args.yaml │ │ └── healthcheck.yaml ├── custom-example │ ├── namespace.yaml │ ├── ingressclass.yaml │ ├── README.md │ └── kustomization.yaml ├── stress-test │ ├── namespace.yaml │ ├── rbac │ │ ├── kustomization.yaml │ │ ├── service_account.yaml │ │ ├── role_binding.yaml │ │ └── role.yaml │ ├── kustomization.yaml │ ├── service.yaml │ ├── config.yaml │ └── deployment.yaml ├── gen_secrets │ ├── service_account.yaml │ ├── kustomization.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── job.yaml ├── clustered-databroker │ ├── service │ │ ├── kustomization.yaml │ │ └── databroker.yaml │ ├── statefulset │ │ ├── image.yaml │ │ ├── kustomization.yaml │ │ ├── resources.yaml │ │ ├── ports.yaml │ │ ├── no-root.yaml │ │ ├── volume.yaml │ │ ├── readonly-root-fs.yaml │ │ ├── base.yaml │ │ └── args.yaml │ └── kustomization.yaml ├── default │ └── kustomization.yaml ├── gateway-api │ ├── gatewayclass.yaml │ ├── kustomization.yaml │ └── role_patch.yaml ├── crd │ ├── files.go │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── prometheus │ ├── gke-podmonitor.yaml │ └── coreos-podmonitor.yaml └── ssh │ └── kustomization.yaml ├── internal ├── .gitignore ├── init.go ├── init_embed.go ├── stress │ ├── echo.go │ └── traffic.go └── filemgr │ ├── filemgr_test.go │ └── filemgr.go ├── .github ├── CODEOWNERS ├── release.yaml ├── PULL_REQUEST_TEMPLATE ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── lint-workflows.yaml │ ├── docs.yaml │ ├── lint.yml │ ├── backport.yaml │ ├── update-dependencies.yaml │ ├── test.yaml │ ├── docker-main.yaml │ ├── release.yaml │ └── docker-version-branches.yaml └── dependabot.yaml ├── .tool-versions ├── docs ├── docs.go ├── templates │ ├── object.tmpl │ ├── objects.tmpl │ ├── header.tmpl │ └── object-properties.tmpl ├── template.go ├── known_formats.go └── cmd │ └── main.go ├── util ├── generic │ ├── doc.go │ ├── gvk.go │ └── builder.go ├── bin_test.go ├── health │ └── check.go ├── merge_map.go ├── secrets.go ├── bin.go ├── merge_map_test.go ├── namespaced_name_test.go ├── namespaced_name.go └── restart_test.go ├── pomerium ├── envoy │ ├── validate.go │ ├── envoy_linux_amd64.go │ ├── envoy_linux_arm64.go │ ├── envoy_darwin_amd64.go │ ├── envoy_darwin_arm64.go │ ├── validate_noop.go │ ├── envoy_test.go │ └── validate_envoy.go ├── tools │ └── get-tools.go ├── deterministic.go ├── ctrl │ ├── config_test.go │ ├── config.go │ ├── run.go │ └── bootstrap_test.go ├── gateway │ ├── matches.go │ ├── backendrefs.go │ └── translate.go ├── routes.go ├── reconcile.go ├── validate.go ├── certs.go ├── cert_map.go ├── route_list.go ├── deterministic_test.go ├── config_test.go └── proto.go ├── .dockerignore ├── Dockerfile ├── Dockerfile.ci ├── scripts ├── check-image-tag.sh ├── update-dependencies ├── open-docs-pull-request.sh └── check-docker-images ├── PROJECT ├── .gitignore ├── main.go ├── controllers ├── mock │ ├── mock.go │ ├── pomerium_config_reconciler.go │ └── pomerium_ingress_reconciler.go ├── ingress │ ├── once.go │ ├── once_test.go │ ├── builder.go │ ├── deps.go │ ├── controller_test.go │ └── ingress_class.go ├── deps │ ├── deps.go │ └── registry_client.go ├── gateway │ ├── conditions.go │ ├── gatewayclass.go │ ├── referencegrant.go │ ├── extensionfilters.go │ └── refkey.go └── reporter │ └── reporter.go ├── cspell.config.yaml ├── README.md ├── apis ├── gateway │ └── v1alpha1 │ │ ├── groupversion_info.go │ │ ├── filter_types.go │ │ └── zz_generated.deepcopy.go └── ingress │ └── v1 │ ├── deprecation_test.go │ ├── groupversion_info.go │ └── deprecation.go ├── cmd ├── root.go ├── controller_test.go ├── databroker_options_test.go ├── databroker_options.go ├── gen_secrets.go ├── common.go └── ingress_opts.go ├── model ├── registry_test.go ├── gateway_config.go └── registry.go └── .pre-commit-config.yaml /config/.gitignore: -------------------------------------------------------------------------------- 1 | dev 2 | -------------------------------------------------------------------------------- /internal/.gitignore: -------------------------------------------------------------------------------- 1 | ui 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pomerium/dev-backend 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golangci-lint 2.7.2 2 | golang 1.25.5 3 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs generates docs from CRD specs 2 | package docs 3 | -------------------------------------------------------------------------------- /config/pomerium/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: pomerium 5 | -------------------------------------------------------------------------------- /internal/init.go: -------------------------------------------------------------------------------- 1 | // Package internal implements few hacks to allow pomerium embedding 2 | package internal 3 | -------------------------------------------------------------------------------- /config/custom-example/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: pomerium-custom 5 | -------------------------------------------------------------------------------- /config/stress-test/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: pomerium-stress-test 5 | -------------------------------------------------------------------------------- /config/gen_secrets/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pomerium-gen-secrets 5 | -------------------------------------------------------------------------------- /config/pomerium/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pomerium-controller 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/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pomerium-stress-test 5 | -------------------------------------------------------------------------------- /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 | 4 | ### `{{.ID}}` 5 | {{template "object" .}} 6 | {{- end}} 7 | {{end}} 8 | -------------------------------------------------------------------------------- /util/generic/doc.go: -------------------------------------------------------------------------------- 1 | // Package generic contains helper functions useful for working with the generic 2 | // controller-runtime APIs. 3 | package generic 4 | -------------------------------------------------------------------------------- /config/clustered-databroker/service/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - databroker.yaml 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/custom-example/ingressclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: IngressClass 3 | metadata: 4 | name: pomerium-2 5 | spec: 6 | controller: pomerium.io/ingress-controller-2 7 | -------------------------------------------------------------------------------- /config/gen_secrets/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - job.yaml 5 | - role_binding.yaml 6 | - role.yaml 7 | - service_account.yaml 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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /pomerium/tools/get-tools.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/pomerium/pomerium/pkg/tools/golangcilint" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | if err := golangcilint.InstallLinter(ctx); err != nil { 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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/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/clustered-databroker/statefulset/image.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | image: pomerium/ingress-controller:main 11 | imagePullPolicy: Always 12 | -------------------------------------------------------------------------------- /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/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 | - path: healthcheck.yaml 13 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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:e51574ad48c5766e7a05b6924bd763953004d48f5725dbd11ebf516d28c1639f 4 | COPY bin/manager /manager 5 | USER 65532:65532 6 | 7 | ENTRYPOINT ["/manager"] 8 | -------------------------------------------------------------------------------- /config/clustered-databroker/statefulset/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: no-root.yaml 9 | - path: ports.yaml 10 | - path: readonly-root-fs.yaml 11 | - path: resources.yaml 12 | - path: volume.yaml 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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:e51574ad48c5766e7a05b6924bd763953004d48f5725dbd11ebf516d28c1639f 4 | ARG TARGETARCH 5 | COPY bin/manager-linux-$TARGETARCH /manager 6 | USER 65532:65532 7 | 8 | ENTRYPOINT ["/manager"] 9 | -------------------------------------------------------------------------------- /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/clustered-databroker/statefulset/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/clustered-databroker/statefulset/ports.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | ports: 11 | - name: grpc 12 | containerPort: 5443 13 | protocol: TCP 14 | - name: raft 15 | containerPort: 5999 16 | protocol: TCP 17 | -------------------------------------------------------------------------------- /config/clustered-databroker/service/databroker.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pomerium-databroker 5 | labels: 6 | app.kubernetes.io/name: pomerium 7 | app.kubernetes.io/component: databroker 8 | spec: 9 | clusterIP: None 10 | ports: 11 | - port: 5443 12 | name: grpc 13 | - port: 5999 14 | name: raft 15 | selector: 16 | app.kubernetes.io/name: pomerium 17 | app.kubernetes.io/component: databroker 18 | -------------------------------------------------------------------------------- /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/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/base.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pomerium 5 | labels: 6 | app.kubernetes.io/name: pomerium 7 | app.kubernetes.io/component: proxy 8 | spec: 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/name: pomerium 14 | app.kubernetes.io/component: proxy 15 | spec: 16 | containers: 17 | - name: pomerium 18 | serviceAccountName: pomerium-controller 19 | terminationGracePeriodSeconds: 10 20 | -------------------------------------------------------------------------------- /util/health/check.go: -------------------------------------------------------------------------------- 1 | // Package health encapsulates extensions to pomerium core's pkg/health 2 | package health 3 | 4 | import ( 5 | "github.com/pomerium/pomerium/pkg/health" 6 | ) 7 | 8 | const ( 9 | // SettingsBootstrapReconciler checks that the bootstrap reconciler has run once successfully 10 | SettingsBootstrapReconciler = health.Check("controller.settings.reconciler.bootstrap") 11 | // SettingsReconciler checks that the leased settings reconciler has run 12 | SettingsReconciler = health.Check("controller.settings.reconciler") 13 | ) 14 | -------------------------------------------------------------------------------- /config/clustered-databroker/statefulset/no-root.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/clustered-databroker/statefulset/volume.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | volumeMounts: 11 | - name: storage 12 | mountPath: /var/pomerium/databroker 13 | volumeClaimTemplates: 14 | - metadata: 15 | name: storage 16 | spec: 17 | accessModes: 18 | - ReadWriteOnce 19 | resources: 20 | requests: 21 | storage: 1Gi 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /config/custom-example/README.md: -------------------------------------------------------------------------------- 1 | This is an example configuration how you can deploy another version of Pomerium Ingress Controller into the cluster, 2 | which may be useful if you're testing a new version upgrade. 3 | 4 | # Configuration 5 | 6 | Each deployment of Pomerium should have their own global settings. 7 | Make sure different deployments of Pomerium never share the same database if you use persistent storage. 8 | 9 | ```yaml 10 | apiVersion: ingress.pomerium.io/v1 11 | kind: Pomerium 12 | metadata: 13 | name: global-2 14 | spec: 15 | runtimeFlags: 16 | mcp: true 17 | secrets: pomerium-2/bootstrap 18 | ``` 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/service/proxy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pomerium-proxy 5 | labels: 6 | app.kubernetes.io/name: pomerium 7 | app.kubernetes.io/component: proxy 8 | spec: 9 | type: LoadBalancer 10 | selector: 11 | app.kubernetes.io/name: pomerium 12 | app.kubernetes.io/component: proxy 13 | ports: 14 | - name: https 15 | port: 443 16 | targetPort: https 17 | protocol: TCP 18 | - name: quic 19 | port: 443 20 | targetPort: quic 21 | protocol: UDP 22 | - name: http 23 | port: 80 24 | targetPort: http 25 | protocol: TCP 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/templates/header.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "header"}} 2 | --- 3 | title: Kubernetes Deployment Reference 4 | sidebar_label: Reference 5 | description: Reference for Pomerium settings in Kubernetes deployments. 6 | --- 7 | 8 | Pomerium-specific parameters should be configured via the `ingress.pomerium.io/Pomerium` CRD. 9 | The default Pomerium deployment is listening to the CRD `global`, that may be customized via command line parameters. 10 | 11 | Pomerium posts updates to the CRD `/status`: 12 | ```shell 13 | kubectl describe pomerium 14 | ``` 15 | 16 | Kubernetes-specific deployment parameters should be added via `kustomize` to the manifests. 17 | 18 | {{end}} 19 | -------------------------------------------------------------------------------- /config/clustered-databroker/statefulset/readonly-root-fs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 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 | -------------------------------------------------------------------------------- /pomerium/deterministic.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "cmp" 5 | "slices" 6 | "sort" 7 | 8 | pb "github.com/pomerium/pomerium/pkg/grpc/config" 9 | ) 10 | 11 | func ensureDeterministicConfigOrder(cfg *pb.Config) { 12 | if cfg == nil { 13 | return 14 | } 15 | // https://kubernetes.io/docs/concepts/services-networking/ingress/#multiple-matches 16 | // envoy matches according to the order routes are present in the configuration 17 | sort.Sort(routeList(cfg.Routes)) 18 | 19 | if len(cfg.GetSettings().GetCertificates()) > 0 { 20 | slices.SortFunc(cfg.Settings.Certificates, func(a, b *pb.Settings_Certificate) int { 21 | return cmp.Compare(a.Id, b.Id) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/custom-example/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: pomerium-2 4 | commonLabels: 5 | app.kubernetes.io/name: pomerium-2 6 | resources: 7 | - ../pomerium/deployment 8 | - ../pomerium/service 9 | - ../pomerium/rbac 10 | - ../gen_secrets 11 | - ingressclass.yaml 12 | - namespace.yaml 13 | patches: 14 | - patch: |- 15 | - op: add 16 | path: /spec/template/spec/containers/0/args/- 17 | value: '--name=pomerium.io/ingress-controller-2' 18 | - op: replace 19 | path: /spec/template/spec/containers/0/args/1 20 | value: '--pomerium-config=global-2' 21 | target: 22 | group: apps 23 | kind: Deployment 24 | name: pomerium 25 | version: v1 26 | -------------------------------------------------------------------------------- /.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/workflows/lint-workflows.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Workflows 2 | permissions: 3 | contents: read 4 | pull-requests: write 5 | on: 6 | push: 7 | paths: 8 | - ".github/workflows/" 9 | pull_request: 10 | paths: 11 | - ".github/workflows/" 12 | 13 | jobs: 14 | check: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v6.0.0 20 | 21 | - name: Install action-validator with asdf 22 | uses: asdf-vm/actions/install@v4 23 | with: 24 | tool_versions: | 25 | action-validator 0.5.1 26 | 27 | - name: Lint Actions 28 | run: | 29 | find .github/workflows -type f \( -iname \*.yaml -o -iname \*.yml \) \ 30 | | xargs -I {} action-validator --verbose {} 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 13 | 14 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 15 | with: 16 | go-version-file: .tool-versions 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 19 | with: 20 | go-version-file: .tool-versions 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@1e7e51e771db61008b38414a730f564565cf7c20 28 | with: 29 | version-file: .tool-versions 30 | args: --timeout=10m 31 | -------------------------------------------------------------------------------- /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 | - --health-probe-bind-address=$(POD_IP):28080 16 | env: 17 | - name: POMERIUM_NAMESPACE 18 | valueFrom: 19 | fieldRef: 20 | apiVersion: v1 21 | fieldPath: metadata.namespace 22 | - name: POD_IP 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: status.podIP 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | startupProbe: 11 | httpGet: 12 | path: /startupz 13 | port : 28080 14 | failureThreshold: 40 15 | periodSeconds: 15 16 | livenessProbe: 17 | httpGet: 18 | path: /healthz 19 | port: 28080 20 | initialDelaySeconds: 15 21 | periodSeconds: 60 22 | failureThreshold: 10 23 | readinessProbe: 24 | httpGet: 25 | path: /readyz 26 | port: 28080 27 | initialDelaySeconds: 15 28 | periodSeconds: 60 29 | failureThreshold: 5 30 | -------------------------------------------------------------------------------- /config/clustered-databroker/statefulset/base.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 5 | spec: 6 | selector: 7 | matchLabels: 8 | app.kubernetes.io/name: pomerium 9 | app.kubernetes.io/component: databroker 10 | serviceName: pomerium-databroker 11 | replicas: 3 12 | podManagementPolicy: Parallel 13 | updateStrategy: 14 | type: RollingUpdate 15 | rollingUpdate: 16 | maxUnavailable: 1 17 | persistentVolumeClaimRetentionPolicy: 18 | whenDeleted: Delete 19 | whenScaled: Delete 20 | template: 21 | metadata: 22 | labels: 23 | app.kubernetes.io/name: pomerium 24 | app.kubernetes.io/component: databroker 25 | spec: 26 | containers: 27 | - name: pomerium 28 | serviceAccountName: pomerium-controller 29 | terminationGracePeriodSeconds: 10 30 | -------------------------------------------------------------------------------- /config/ssh/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: pomerium 2 | commonLabels: 3 | app.kubernetes.io/name: pomerium 4 | resources: 5 | - ../default 6 | patches: 7 | - patch: |- 8 | - op: add 9 | path: /spec/template/spec/containers/0/args/- 10 | value: '--ssh-addr=:4022' 11 | - op: add 12 | path: /spec/template/spec/containers/0/ports/- 13 | value: 14 | name: ssh 15 | containerPort: 4022 16 | protocol: TCP 17 | target: 18 | group: apps 19 | version: v1 20 | kind: Deployment 21 | name: pomerium 22 | - patch: |- 23 | - op: add 24 | path: /spec/ports/- 25 | value: 26 | name: ssh 27 | targetPort: ssh 28 | protocol: TCP 29 | port: 4022 30 | target: 31 | version: v1 32 | kind: Service 33 | name: pomerium-proxy 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/update-dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | _project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/.." 5 | 6 | require-command() { 7 | local _command="${1?"command is required"}" 8 | 9 | if ! command -v "$_command" >/dev/null 2>&1; then 10 | echo "$_command is required" 11 | exit 1 12 | fi 13 | } 14 | 15 | update-pomerium() { 16 | pushd "$_project_root" 17 | 18 | require-command go 19 | 20 | go get -u github.com/pomerium/pomerium@main 21 | go mod tidy 22 | 23 | popd 24 | } 25 | 26 | update-tools() { 27 | pushd "$_project_root" 28 | 29 | require-command asdf 30 | 31 | asdf install golang latest:1.25 32 | asdf set golang latest:1.25 33 | asdf install golangci-lint latest:2 34 | asdf set golangci-lint latest:2 35 | 36 | popd 37 | } 38 | 39 | run() { 40 | local _command="$1" 41 | case "$_command" in 42 | pomerium) 43 | update-pomerium 44 | ;; 45 | tools) 46 | update-tools 47 | ;; 48 | *) 49 | echo "unknown command $_command" 50 | exit 1 51 | ;; 52 | esac 53 | } 54 | 55 | run "${1?'command is required'}" 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/clustered-databroker/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: pomerium 2 | resources: 3 | - ../default 4 | - ./statefulset 5 | - ./service 6 | patches: 7 | - patch: |- 8 | - op: add 9 | path: /spec/template/spec/containers/0/args/- 10 | value: '--services=authenticate,authorize,proxy' 11 | - op: add 12 | path: /spec/template/spec/containers/0/args/- 13 | value: '--databroker-service-urls=https://pomerium-databroker-0.pomerium-databroker:5443' 14 | - op: add 15 | path: /spec/template/spec/containers/0/args/- 16 | value: '--databroker-service-urls=https://pomerium-databroker-1.pomerium-databroker:5443' 17 | - op: add 18 | path: /spec/template/spec/containers/0/args/- 19 | value: '--databroker-service-urls=https://pomerium-databroker-2.pomerium-databroker:5443' 20 | - op: add 21 | path: /spec/template/spec/containers/0/args/- 22 | value: '--databroker-auto-tls=*.pomerium-databroker' 23 | target: 24 | group: apps 25 | version: v1 26 | kind: Deployment 27 | name: pomerium 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/templates/object-properties.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "object-properties" }} 2 | {{- if . }} 3 | 4 | 5 | 6 | 7 | {{- range .}} 8 | 9 | 39 | 40 | {{- end}} 41 | 42 |
10 |

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

27 |

28 | {{- if .Required}} 29 | Required.  30 | {{- end}} 31 | {{- if .Description}} 32 | {{.Description}} 33 | {{- end}} 34 |

35 | {{- if and .Atomic .Atomic.ExplainFormat}} 36 | Format: {{.Atomic.ExplainFormat}} 37 | {{- end}} 38 |
43 | {{- end }} 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /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", "127.0.0.1:8004", 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 | -------------------------------------------------------------------------------- /.github/workflows/update-dependencies.yaml: -------------------------------------------------------------------------------- 1 | name: Update Dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "40 1 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-dependencies: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 14 | 15 | - name: Setup ASDF 16 | uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 17 | 18 | - name: Update Tools 19 | run: ./scripts/update-dependencies tools 20 | 21 | - name: Update Pomerium Dependencies 22 | run: ./scripts/update-dependencies pomerium 23 | 24 | - name: Generate 25 | run: make generate 26 | 27 | - name: Check for Changes 28 | id: git-diff 29 | run: | 30 | git config --global user.email "apparitor@users.noreply.github.com" 31 | git config --global user.name "GitHub Actions" 32 | git add . 33 | git diff --cached --exit-code || echo "changed=true" >> $GITHUB_OUTPUT 34 | 35 | - name: Create Pull Request 36 | if: ${{ steps.git-diff.outputs.changed }} == 'true' 37 | uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 38 | with: 39 | author: GitHub Actions 40 | body: "This PR updates dependencies not managed by dependabot." 41 | branch: ci/update-core 42 | commit-message: "ci: update dependencies" 43 | delete-branch: true 44 | labels: ci 45 | title: "ci: update dependencies" 46 | token: ${{ secrets.APPARITOR_GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/clustered-databroker/statefulset/args.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: pomerium-databroker 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: pomerium 10 | args: 11 | - all-in-one 12 | - --pomerium-config=global 13 | - --metrics-bind-address=$(POD_IP):9090 14 | - --services=databroker 15 | - --databroker-cluster-node-id=$(POD_NAME) 16 | - --databroker-raft-bind-address=:5999 17 | - --databroker-auto-tls=*.pomerium-databroker 18 | env: 19 | - name: POMERIUM_NAMESPACE 20 | valueFrom: 21 | fieldRef: 22 | apiVersion: v1 23 | fieldPath: metadata.namespace 24 | - name: POD_IP 25 | valueFrom: 26 | fieldRef: 27 | apiVersion: v1 28 | fieldPath: status.podIP 29 | - name: POD_NAME 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: DATABROKER_CLUSTER_NODES 34 | value: |- 35 | [ 36 | { "id": "pomerium-databroker-0", "grpc_address": "https://pomerium-databroker-0.pomerium-databroker:5443", "raft_address": "pomerium-databroker-0.pomerium-databroker:5999" }, 37 | { "id": "pomerium-databroker-1", "grpc_address": "https://pomerium-databroker-1.pomerium-databroker:5443", "raft_address": "pomerium-databroker-1.pomerium-databroker:5999" }, 38 | { "id": "pomerium-databroker-2", "grpc_address": "https://pomerium-databroker-2.pomerium-databroker:5443", "raft_address": "pomerium-databroker-2.pomerium-databroker:5999" } 39 | ] 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 19 | with: 20 | go-version-file: .tool-versions 21 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 38 | with: 39 | fetch-depth: 0 40 | 41 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 42 | with: 43 | go-version-file: .tool-versions 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 | 52 | build: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 56 | 57 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 58 | with: 59 | go-version-file: .tool-versions 60 | 61 | - name: build 62 | run: make build 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 20 | with: 21 | go-version-file: .tool-versions 22 | 23 | - name: Docker meta 24 | id: meta 25 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 26 | with: 27 | # list of Docker images to use as base name for tags 28 | images: | 29 | pomerium/ingress-controller 30 | docker.cloudsmith.io/pomerium/ingress-controller/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 Docker Buildx 37 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 38 | 39 | - name: Login to DockerHub 40 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 41 | with: 42 | username: ${{ secrets.DOCKERHUB_USER }} 43 | password: ${{ secrets.DOCKERHUB_TOKEN }} 44 | 45 | - name: Log in to cloudsmith registry 46 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 47 | with: 48 | registry: docker.cloudsmith.io 49 | username: ${{ secrets.CLOUDSMITH_USER }} 50 | password: ${{ secrets.CLOUDSMITH_API_KEY }} 51 | 52 | - name: Build 53 | run: make build-ci 54 | 55 | - name: Docker Publish - Main 56 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 57 | with: 58 | context: . 59 | file: ./Dockerfile.ci 60 | push: true 61 | platforms: linux/amd64,linux/arm64 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /cmd/databroker_options_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-playground/validator/v10" 7 | "github.com/spf13/pflag" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/volatiletech/null/v9" 10 | ) 11 | 12 | func TestDataBrokerOptions(t *testing.T) { 13 | t.Parallel() 14 | 15 | parseOptions := func(arguments []string) *dataBrokerOptions { 16 | flags := pflag.NewFlagSet("test", pflag.PanicOnError) 17 | options := &dataBrokerOptions{} 18 | options.setupFlags(flags) 19 | err := flags.Parse(arguments) 20 | assert.NoError(t, err) 21 | return options 22 | } 23 | 24 | options := parseOptions([]string{ 25 | "--databroker-cluster-node-id", "node-2", 26 | "--databroker-cluster-nodes", `[ 27 | { "id": "node-0", "grpc_address": "127.0.0.1:15000", "raft_address": "127.0.0.1:15100" }, 28 | { "id": "node-1", "grpc_address": "127.0.0.1:15001", "raft_address": "127.0.0.1:15101" }, 29 | { "id": "node-2", "grpc_address": "127.0.0.1:15002", "raft_address": "127.0.0.1:15102" } 30 | ]`, 31 | "--databroker-raft-bind-address=:15001", 32 | "--databroker-service-urls=https://databroker-1.example.com", 33 | "--databroker-service-urls=https://databroker-2.example.com", 34 | "--databroker-service-urls=https://databroker-3.example.com", 35 | }) 36 | assert.Equal(t, &dataBrokerOptions{ 37 | ClusterNodeID: "node-2", 38 | ClusterNodes: dataBrokerClusterNodes{ 39 | {ID: "node-0", GRPCAddress: "127.0.0.1:15000", RaftAddress: null.StringFrom("127.0.0.1:15100")}, 40 | {ID: "node-1", GRPCAddress: "127.0.0.1:15001", RaftAddress: null.StringFrom("127.0.0.1:15101")}, 41 | {ID: "node-2", GRPCAddress: "127.0.0.1:15002", RaftAddress: null.StringFrom("127.0.0.1:15102")}, 42 | }, 43 | RaftBindAddress: ":15001", 44 | ServiceURLs: []string{ 45 | "https://databroker-1.example.com", 46 | "https://databroker-2.example.com", 47 | "https://databroker-3.example.com", 48 | }, 49 | }, options) 50 | assert.NoError(t, validator.New().Struct(options)) 51 | 52 | assert.NoError(t, validator.New().Struct(parseOptions([]string{}))) 53 | assert.Error(t, validator.New().Struct(parseOptions([]string{ 54 | "--databroker-raft-bind-address", "", 55 | }))) 56 | assert.Error(t, validator.New().Struct(parseOptions([]string{ 57 | "--databroker-service-urls", "", 58 | }))) 59 | } 60 | -------------------------------------------------------------------------------- /.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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 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@c299e40c65443455700f0fdfc63efafe5b349051 26 | with: 27 | images: | 28 | pomerium/ingress-controller 29 | docker.cloudsmith.io/pomerium/ingress-controller/ingress-controller 30 | tags: | 31 | type=semver,pattern={{raw}} 32 | type=semver,pattern=v{{major}}.{{minor}} 33 | type=sha 34 | 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 37 | 38 | - name: Login to DockerHub 39 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 40 | with: 41 | username: ${{ secrets.DOCKERHUB_USER }} 42 | password: ${{ secrets.DOCKERHUB_TOKEN }} 43 | 44 | - name: Log in to cloudsmith registry 45 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 46 | with: 47 | registry: docker.cloudsmith.io 48 | username: ${{ secrets.CLOUDSMITH_USER }} 49 | password: ${{ secrets.CLOUDSMITH_API_KEY }} 50 | 51 | - name: Setup Go 52 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 53 | with: 54 | go-version-file: .tool-versions 55 | 56 | - name: Build 57 | run: make build-ci 58 | 59 | - name: Docker Publish - Main 60 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 61 | with: 62 | context: . 63 | file: ./Dockerfile.ci 64 | push: true 65 | platforms: linux/amd64,linux/arm64 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | -------------------------------------------------------------------------------- /.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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Go 20 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 21 | with: 22 | go-version-file: .tool-versions 23 | 24 | - name: Docker meta 25 | id: meta 26 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 27 | with: 28 | # list of Docker images to use as base name for tags 29 | images: | 30 | pomerium/ingress-controller 31 | docker.cloudsmith.io/pomerium/ingress-controller/ingress-controller 32 | # generate Docker tags based on the following events/attributes 33 | tags: | 34 | type=ref,event=branch 35 | type=sha 36 | 37 | - name: Set up QEMU 38 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 39 | 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 42 | 43 | - name: Login to DockerHub 44 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 45 | with: 46 | username: ${{ secrets.DOCKERHUB_USER }} 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | 49 | - name: Log in to cloudsmith registry 50 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 51 | with: 52 | registry: docker.cloudsmith.io 53 | username: ${{ secrets.CLOUDSMITH_USER }} 54 | password: ${{ secrets.CLOUDSMITH_API_KEY }} 55 | 56 | - name: Build 57 | run: make build-ci 58 | 59 | - name: Docker Publish - Version Branches 60 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 61 | with: 62 | context: . 63 | file: ./Dockerfile.ci 64 | push: true 65 | platforms: linux/amd64,linux/arm64 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /cmd/databroker_options.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "net" 8 | "net/url" 9 | 10 | "github.com/spf13/pflag" 11 | "github.com/volatiletech/null/v9" 12 | "google.golang.org/grpc" 13 | "gopkg.in/yaml.v3" 14 | 15 | "github.com/pomerium/pomerium/config" 16 | "github.com/pomerium/pomerium/pkg/grpcutil" 17 | ) 18 | 19 | type dataBrokerOptions struct { 20 | ClusterNodeID string 21 | ClusterNodes dataBrokerClusterNodes 22 | RaftBindAddress string `validate:"omitempty,hostname_port"` 23 | ServiceURLs []string `validate:"dive,url"` 24 | } 25 | 26 | // dataBrokerClusterNodes is a custom type to support reading cluster nodes from yaml 27 | type dataBrokerClusterNodes []config.DataBrokerClusterNode 28 | 29 | func (n dataBrokerClusterNodes) MarshalText() ([]byte, error) { 30 | return yaml.Marshal([]config.DataBrokerClusterNode(n)) 31 | } 32 | 33 | func (n *dataBrokerClusterNodes) UnmarshalText(text []byte) error { 34 | return yaml.Unmarshal(text, (*[]config.DataBrokerClusterNode)(n)) 35 | } 36 | 37 | func (o *dataBrokerOptions) setupFlags(flags *pflag.FlagSet) { 38 | flags.StringVar(&o.ClusterNodeID, "databroker-cluster-node-id", "", "the databroker cluster node id") 39 | flags.TextVar(&o.ClusterNodes, "databroker-cluster-nodes", dataBrokerClusterNodes(nil), "the databroker cluster nodes") 40 | flags.StringVar(&o.RaftBindAddress, "databroker-raft-bind-address", "", "the databroker raft bind address") 41 | flags.StringSliceVar(&o.ServiceURLs, "databroker-service-urls", nil, "the databroker service urls, defaults to localhost") 42 | } 43 | 44 | func (o *dataBrokerOptions) apply(dst *config.Config) { 45 | if o.ClusterNodeID != "" { 46 | dst.Options.DataBroker.ClusterNodeID = null.StringFrom(o.ClusterNodeID) 47 | } 48 | dst.Options.DataBroker.ClusterNodes = config.DataBrokerClusterNodes(o.ClusterNodes) 49 | if o.RaftBindAddress != "" { 50 | dst.Options.DataBroker.RaftBindAddress = null.StringFrom(o.RaftBindAddress) 51 | } 52 | dst.Options.DataBroker.ServiceURLs = o.ServiceURLs 53 | } 54 | 55 | func getDataBrokerConnection(ctx context.Context, cfg *config.Config) (*grpc.ClientConn, error) { 56 | sharedSecret, err := base64.StdEncoding.DecodeString(cfg.Options.SharedKey) 57 | if err != nil { 58 | return nil, fmt.Errorf("decode shared_secret: %w", err) 59 | } 60 | 61 | // use the local gRPC port 62 | dataBrokerURL := &url.URL{Scheme: "http", Host: net.JoinHostPort("127.0.0.1", cfg.GRPCPort)} 63 | // if a databroker service url is set, use the outbound port instead 64 | if len(cfg.Options.DataBroker.ServiceURLs) > 0 || cfg.Options.DataBroker.ServiceURL != "" { 65 | dataBrokerURL = &url.URL{Scheme: "http", Host: net.JoinHostPort("127.0.0.1", cfg.OutboundPort)} 66 | } 67 | 68 | return grpcutil.NewGRPCClientConn(ctx, &grpcutil.Options{ 69 | Address: dataBrokerURL, 70 | ServiceName: "databroker", 71 | SignedJWTKey: sharedSecret, 72 | RequestTimeout: defaultGRPCTimeout, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 := *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 | -------------------------------------------------------------------------------- /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 | metricsBindAddress = "metrics-bind-address" 33 | healthProbeBindAddress = "health-probe-bind-address" 34 | ) 35 | 36 | func envName(name string) string { 37 | return strcase.ToScreamingSnake(name) 38 | } 39 | 40 | func setupLogger(debug bool) { 41 | level := zapcore.InfoLevel 42 | if debug { 43 | level = zapcore.DebugLevel 44 | } 45 | opts := zap.Options{ 46 | Level: level, 47 | StacktraceLevel: zapcore.DPanicLevel, 48 | } 49 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 50 | } 51 | 52 | func getScheme() (*runtime.Scheme, error) { 53 | scheme := runtime.NewScheme() 54 | for _, apply := range []struct { 55 | name string 56 | fn func(*runtime.Scheme) error 57 | }{ 58 | {"core", clientgoscheme.AddToScheme}, 59 | {"settings", icsv1.AddToScheme}, 60 | {"gateway_v1", gateway_v1.Install}, 61 | {"gateway_v1beta1", gateway_v1beta1.Install}, 62 | {"gateway.pomerium.io", icgv1alpha1.AddToScheme}, 63 | } { 64 | if err := apply.fn(scheme); err != nil { 65 | return nil, fmt.Errorf("%s: %w", apply.name, err) 66 | } 67 | } 68 | return scheme, nil 69 | } 70 | 71 | func viperWalk(flags *pflag.FlagSet) error { 72 | v := viper.New() 73 | var errs *multierror.Error 74 | flags.VisitAll(func(f *pflag.Flag) { 75 | if err := v.BindEnv(f.Name, envName(f.Name)); err != nil { 76 | errs = multierror.Append(errs, err) 77 | return 78 | } 79 | 80 | if !f.Changed && v.IsSet(f.Name) { 81 | val := v.Get(f.Name) 82 | errs = multierror.Append(errs, flags.Set(f.Name, fmt.Sprintf("%v", val))) 83 | } 84 | }) 85 | return errs.ErrorOrNil() 86 | } 87 | 88 | func runHealthz(ctx context.Context, addr string, readyChecks ...healthz.HealthChecker) error { 89 | ctx, cancel := context.WithCancel(ctx) 90 | defer cancel() 91 | 92 | mux := http.NewServeMux() 93 | healthz.InstallHandler(mux) 94 | healthz.InstallReadyzHandler(mux, readyChecks...) 95 | 96 | srv := http.Server{ 97 | Addr: addr, 98 | Handler: mux, 99 | ReadHeaderTimeout: time.Millisecond * 100, 100 | } 101 | go func() { 102 | <-ctx.Done() 103 | _ = srv.Close() 104 | }() 105 | 106 | return srv.ListenAndServe() 107 | } 108 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pomerium/deterministic_test.go: -------------------------------------------------------------------------------- 1 | package pomerium 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "google.golang.org/protobuf/proto" 8 | "google.golang.org/protobuf/testing/protocmp" 9 | 10 | configpb "github.com/pomerium/pomerium/pkg/grpc/config" 11 | ) 12 | 13 | func TestEnsureDeterministicConfigOrder(t *testing.T) { 14 | t.Parallel() 15 | 16 | testCases := []struct { 17 | name string 18 | cfg *configpb.Config 19 | want *configpb.Config 20 | }{ 21 | { 22 | name: "sorts routes and certificates", 23 | cfg: &configpb.Config{ 24 | Routes: []*configpb.Route{ 25 | {Name: "route-b", From: "https://b.example.com"}, 26 | {Name: "route-a", From: "https://a.example.com"}, 27 | }, 28 | Settings: &configpb.Settings{ 29 | Certificates: []*configpb.Settings_Certificate{ 30 | {Id: "cert-b"}, 31 | {Id: "cert-a"}, 32 | }, 33 | }, 34 | }, 35 | want: &configpb.Config{ 36 | Routes: []*configpb.Route{ 37 | {Name: "route-a", From: "https://a.example.com"}, 38 | {Name: "route-b", From: "https://b.example.com"}, 39 | }, 40 | Settings: &configpb.Settings{ 41 | Certificates: []*configpb.Settings_Certificate{ 42 | {Id: "cert-a"}, 43 | {Id: "cert-b"}, 44 | }, 45 | }, 46 | }, 47 | }, 48 | { 49 | name: "sorts routes with identical hosts", 50 | cfg: &configpb.Config{ 51 | Routes: []*configpb.Route{ 52 | { 53 | Name: "route-root", 54 | From: "https://a.example.com", 55 | Path: "/", 56 | }, 57 | { 58 | Name: "route-deep", 59 | From: "https://a.example.com", 60 | Path: "/nested", 61 | }, 62 | }, 63 | }, 64 | want: &configpb.Config{ 65 | Routes: []*configpb.Route{ 66 | { 67 | Name: "route-deep", 68 | From: "https://a.example.com", 69 | Path: "/nested", 70 | }, 71 | { 72 | Name: "route-root", 73 | From: "https://a.example.com", 74 | Path: "/", 75 | }, 76 | }, 77 | }, 78 | }, 79 | { 80 | name: "leaves sorted config untouched", 81 | cfg: &configpb.Config{ 82 | Routes: []*configpb.Route{ 83 | {Name: "route-a", From: "https://a.example.com"}, 84 | }, 85 | Settings: &configpb.Settings{ 86 | Certificates: []*configpb.Settings_Certificate{ 87 | {Id: "cert-a"}, 88 | }, 89 | }, 90 | }, 91 | want: &configpb.Config{ 92 | Routes: []*configpb.Route{ 93 | {Name: "route-a", From: "https://a.example.com"}, 94 | }, 95 | Settings: &configpb.Settings{ 96 | Certificates: []*configpb.Settings_Certificate{ 97 | {Id: "cert-a"}, 98 | }, 99 | }, 100 | }, 101 | }, 102 | } 103 | 104 | for _, tc := range testCases { 105 | t.Run(tc.name, func(t *testing.T) { 106 | t.Parallel() 107 | 108 | cfg := proto.Clone(tc.cfg).(*configpb.Config) 109 | ensureDeterministicConfigOrder(cfg) 110 | 111 | if diff := cmp.Diff(tc.want, cfg, protocmp.Transform()); diff != "" { 112 | t.Fatalf("unexpected config (-want +got):\n%s", diff) 113 | } 114 | 115 | again := proto.Clone(cfg).(*configpb.Config) 116 | ensureDeterministicConfigOrder(again) 117 | 118 | if diff := cmp.Diff(cfg, again, protocmp.Transform()); diff != "" { 119 | t.Fatalf("ensureDeterministicConfigOrder not idempotent (-first +second):\n%s", diff) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | name := md.FullName() 41 | switch name { 42 | case "google.protobuf.Duration": 43 | // convert go duration strings into protojson duration strings 44 | if v, ok := data.(string); ok { 45 | return goDurationStringToProtoJSONDurationString(v) 46 | } 47 | case "pomerium.config.Route.StringList": 48 | if v, ok := data.([]any); ok { 49 | return map[string]any{"values": v} 50 | } 51 | case "envoy.type.v3.Percent": 52 | // convert percentage value to percentage message {"value" : } 53 | v, ok := data.(float64) 54 | if ok { 55 | return map[string]float64{ 56 | "value": v, 57 | } 58 | } 59 | fallthrough 60 | default: 61 | // preprocess all the fields 62 | if v, ok := data.(map[string]any); ok { 63 | fds := md.Fields() 64 | for i := 0; i < fds.Len(); i++ { 65 | fd := fds.Get(i) 66 | name := string(fd.Name()) 67 | vv, ok := v[name] 68 | if ok { 69 | v[name] = preprocessAnnotationField(fd, vv) 70 | } 71 | } 72 | return v 73 | } 74 | } 75 | return data 76 | } 77 | 78 | func preprocessAnnotationField(fd protoreflect.FieldDescriptor, data any) any { 79 | if fd.Enum() != nil && fd.Enum().FullName() == "pomerium.config.BearerTokenFormat" { 80 | if v, ok := data.(string); ok { 81 | switch v { 82 | case "": 83 | return "BEARER_TOKEN_FORMAT_UNKNOWN" 84 | case "default": 85 | return "BEARER_TOKEN_FORMAT_DEFAULT" 86 | case "idp_access_token": 87 | return "BEARER_TOKEN_FORMAT_IDP_ACCESS_TOKEN" 88 | case "idp_identity_token": 89 | return "BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN" 90 | } 91 | } 92 | } 93 | // if this is a repeated field, handle each of the field values separately 94 | if fd.IsList() { 95 | vs, ok := data.([]any) 96 | if ok { 97 | nvs := make([]any, len(vs)) 98 | for i, v := range vs { 99 | nvs[i] = preprocessAnnotationFieldValue(fd, v) 100 | } 101 | return nvs 102 | } 103 | } 104 | 105 | return preprocessAnnotationFieldValue(fd, data) 106 | } 107 | 108 | func preprocessAnnotationFieldValue(fd protoreflect.FieldDescriptor, data any) any { 109 | // convert map[string]any -> map[string]string 110 | if fd.IsMap() && fd.MapKey().Kind() == protoreflect.StringKind && fd.MapValue().Kind() == protoreflect.StringKind { 111 | if v, ok := data.(map[string]any); ok { 112 | m := make(map[string]string, len(v)) 113 | for k, vv := range v { 114 | m[k] = fmt.Sprint(vv) 115 | } 116 | return m 117 | } 118 | } 119 | 120 | switch fd.Kind() { 121 | case protoreflect.MessageKind: 122 | return preprocessAnnotationMessage(fd.Message(), data) 123 | } 124 | 125 | return data 126 | } 127 | 128 | func goDurationStringToProtoJSONDurationString(in string) string { 129 | dur, err := time.ParseDuration(in) 130 | if err != nil { 131 | return in 132 | } 133 | 134 | bs, err := protojson.Marshal(durationpb.New(dur)) 135 | if err != nil { 136 | return in 137 | } 138 | 139 | str := strings.Trim(string(bs), `"`) 140 | return str 141 | } 142 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------