From 165eddcc93000a9aed672a5e30ac1ba8acf3c566 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Wed, 4 Dec 2024 18:47:03 +0100 Subject: [PATCH] use TokenReview API to Authenticate requests (#9) * use TokenReview API to Authenticate requests Signed-off-by: Francesco Ilario * support auth in proxy Signed-off-by: Francesco Ilario * tests for dumb and intelligent proxy arch Signed-off-by: Francesco Ilario * move to dumb and smart Signed-off-by: Francesco Ilario * make tests runnable in parallel Signed-off-by: Francesco Ilario * update readmes Signed-off-by: Francesco Ilario * fix tests Signed-off-by: Francesco Ilario * fix linter Signed-off-by: Francesco Ilario * cleanup Signed-off-by: Francesco Ilario * fix typo in smart-proxy's Makefile Signed-off-by: Francesco Ilario * simplify KUBECONFIG in Makefile Signed-off-by: Francesco Ilario * remove TMPDIR in favor of OUT_DIR Signed-off-by: Francesco Ilario * forward ginkgo context in test runs Signed-off-by: Francesco Ilario * add mockgen Signed-off-by: Francesco Ilario * add tests for authenticator Signed-off-by: Francesco Ilario * fix Makefile Signed-off-by: Francesco Ilario * no lint on ginkgo context Signed-off-by: Francesco Ilario * remove sleep Signed-off-by: Francesco Ilario * Update acceptance/make/common.mk Co-authored-by: Andy Sadler --------- Signed-off-by: Francesco Ilario Co-authored-by: Andy Sadler --- Makefile | 13 +- README.md | 25 +-- acceptance/Makefile | 92 ---------- acceptance/README.md | 22 +++ .../config/acceptance-tests/namespace.yaml | 2 + acceptance/features/list.feature | 5 - acceptance/features/read.feature | 5 - acceptance/go.mod | 24 ++- acceptance/go.sum | 60 +++---- acceptance/hook_test.go | 17 -- acceptance/make/common.mk | 71 ++++++++ acceptance/pkg/context/context.go | 32 +++- acceptance/pkg/context/user_info.go | 44 +++++ acceptance/pkg/rest/client.go | 12 +- acceptance/pkg/suite/env.go | 12 ++ acceptance/pkg/suite/hooks.go | 56 ++++++ .../{step_test.go => pkg/suite/steps.go} | 64 +++---- acceptance/test/dumb-proxy/.gitignore | 1 + acceptance/test/dumb-proxy/Makefile | 50 ++++++ acceptance/test/dumb-proxy/README.md | 5 + .../dumb-proxy}/config/proxy/certificate.yaml | 0 .../config/proxy/kustomization.yaml | 0 .../dumb-proxy}/config/proxy/nginx.conf | 4 +- .../dumb-proxy}/config/proxy/proxy.yaml | 8 +- .../dumb-proxy}/config/proxy/rbac.yaml | 0 .../test/dumb-proxy/features/list.feature | 5 + .../test/dumb-proxy/features/read.feature | 5 + acceptance/test/dumb-proxy/hook_test.go | 37 ++++ .../{ => test/dumb-proxy}/kind-config.yaml | 2 - acceptance/test/dumb-proxy/step_test.go | 11 ++ .../{ => test/dumb-proxy}/suite_test.go | 0 acceptance/test/smart-proxy/.gitignore | 1 + acceptance/test/smart-proxy/Makefile | 58 ++++++ acceptance/test/smart-proxy/README.md | 11 ++ .../config/proxy-auth/certificate.yaml | 11 ++ .../config/proxy-auth/kustomization.yaml | 11 ++ .../smart-proxy/config/proxy-auth/nginx.conf | 100 +++++++++++ .../smart-proxy/config/proxy-auth/proxy.yaml | 169 ++++++++++++++++++ .../smart-proxy/config/proxy-auth/rbac.yaml | 41 +++++ .../test/smart-proxy/features/list.feature | 5 + .../test/smart-proxy/features/read.feature | 1 + acceptance/test/smart-proxy/hook_test.go | 38 ++++ acceptance/test/smart-proxy/kind-config.yaml | 16 ++ acceptance/test/smart-proxy/step_test.go | 11 ++ acceptance/test/smart-proxy/suite_test.go | 63 +++++++ authenticator.go | 93 ++++++++++ authenticator_test.go | 107 +++++++++++ cache.go | 6 +- config/deployment.yaml | 14 +- config/patches/with-header-auth.yaml | 5 + config/rbac.yaml | 14 ++ const.go | 2 +- context.go | 3 +- env.go | 4 - go.mod | 24 ++- go.sum | 41 ++++- hack/tools/mockgen/go.mod | 11 ++ hack/tools/mockgen/go.sum | 16 ++ hack/tools/mockgen/tools.go | 9 + http_handler_list.go | 13 +- http_handler_list_test.go | 39 ++-- http_server.go | 34 +++- interfaces_test.go | 10 ++ log.go | 2 +- main.go | 18 +- mocks/rest_interface.go | 156 ++++++++++++++++ namespacelister_test.go | 8 +- 67 files changed, 1551 insertions(+), 298 deletions(-) delete mode 100644 acceptance/Makefile create mode 100644 acceptance/README.md delete mode 100644 acceptance/features/list.feature delete mode 100644 acceptance/features/read.feature delete mode 100644 acceptance/hook_test.go create mode 100644 acceptance/make/common.mk create mode 100644 acceptance/pkg/context/user_info.go create mode 100644 acceptance/pkg/suite/env.go create mode 100644 acceptance/pkg/suite/hooks.go rename acceptance/{step_test.go => pkg/suite/steps.go} (69%) create mode 100644 acceptance/test/dumb-proxy/.gitignore create mode 100644 acceptance/test/dumb-proxy/Makefile create mode 100644 acceptance/test/dumb-proxy/README.md rename acceptance/{ => test/dumb-proxy}/config/proxy/certificate.yaml (100%) rename acceptance/{ => test/dumb-proxy}/config/proxy/kustomization.yaml (100%) rename acceptance/{ => test/dumb-proxy}/config/proxy/nginx.conf (97%) rename acceptance/{ => test/dumb-proxy}/config/proxy/proxy.yaml (96%) rename acceptance/{ => test/dumb-proxy}/config/proxy/rbac.yaml (100%) create mode 100644 acceptance/test/dumb-proxy/features/list.feature create mode 100644 acceptance/test/dumb-proxy/features/read.feature create mode 100644 acceptance/test/dumb-proxy/hook_test.go rename acceptance/{ => test/dumb-proxy}/kind-config.yaml (84%) create mode 100644 acceptance/test/dumb-proxy/step_test.go rename acceptance/{ => test/dumb-proxy}/suite_test.go (100%) create mode 100644 acceptance/test/smart-proxy/.gitignore create mode 100644 acceptance/test/smart-proxy/Makefile create mode 100644 acceptance/test/smart-proxy/README.md create mode 100644 acceptance/test/smart-proxy/config/proxy-auth/certificate.yaml create mode 100644 acceptance/test/smart-proxy/config/proxy-auth/kustomization.yaml create mode 100644 acceptance/test/smart-proxy/config/proxy-auth/nginx.conf create mode 100644 acceptance/test/smart-proxy/config/proxy-auth/proxy.yaml create mode 100644 acceptance/test/smart-proxy/config/proxy-auth/rbac.yaml create mode 100644 acceptance/test/smart-proxy/features/list.feature create mode 100644 acceptance/test/smart-proxy/features/read.feature create mode 100644 acceptance/test/smart-proxy/hook_test.go create mode 100644 acceptance/test/smart-proxy/kind-config.yaml create mode 100644 acceptance/test/smart-proxy/step_test.go create mode 100644 acceptance/test/smart-proxy/suite_test.go create mode 100644 authenticator.go create mode 100644 authenticator_test.go create mode 100644 config/patches/with-header-auth.yaml create mode 100644 hack/tools/mockgen/go.mod create mode 100644 hack/tools/mockgen/go.sum create mode 100644 hack/tools/mockgen/tools.go create mode 100644 interfaces_test.go create mode 100644 mocks/rest_interface.go diff --git a/Makefile b/Makefile index 383c80e..038c560 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ -ROOT_DIR := $(realpath $(firstword $(MAKEFILE_LIST))) +ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) LOCALBIN := $(ROOT_DIR)/bin OUTDIR := $(ROOT_DIR)/out GO ?= go -GOLANG_CI ?= $(GO) run -modfile $(shell dirname $(ROOT_DIR))/hack/tools/golang-ci/go.mod github.com/golangci/golangci-lint/cmd/golangci-lint +GOLANG_CI ?= $(GO) run -modfile $(ROOT_DIR)/hack/tools/golang-ci/go.mod github.com/golangci/golangci-lint/cmd/golangci-lint IMG ?= namespace-lister:latest IMAGE_BUILDER ?= docker @@ -51,3 +51,12 @@ test: ## Run go test against code. .PHONY: image-build image-build: $(IMAGE_BUILDER) build -t "$(IMG)" . + +.PHONY: generate-code +generate-code: mockgen ## Run go generate on the project. + @echo $(GO) generate ./... + @PATH=$(LOCALBIN):${PATH} $(GO) generate ./... + +.PHONY: mockgen +mockgen: $(LOCALBIN) ## Install mockgen locally. + $(GO) build -modfile $(ROOT_DIR)/hack/tools/mockgen/go.mod -o $(LOCALBIN)/mockgen go.uber.org/mock/mockgen diff --git a/README.md b/README.md index 0e3bdd3..88e8ebc 100644 --- a/README.md +++ b/README.md @@ -48,20 +48,6 @@ subjects: name: user ``` -## Try - -The easiest way of trying this component locally is using `make -C acceptance prepare`. -This command will build the image, create the Kind cluster, load the image in it, and deploy all needed components. - -You will find a valid `kubeconfig` that you can use in `/tmp/namespace-lister-acceptance-tests-user.kcfg`. -To access the namespace-lister you need to target the url `https://localhost:10443` and skip TLS verification. - -``` -KUBECONFIG=/tmp/namespace-lister-acceptance-tests-user.kcfg kubectl get namespaces --server=https://localhost:10443 --insecure-skip-tls-verify -``` - -Take a look at the [Tests section](#tests) for more info. - ## Tests Acceptance tests are implemented in the [acceptance folder](./acceptance/). @@ -69,15 +55,12 @@ Acceptance tests are implemented in the [acceptance folder](./acceptance/). Behavior-Driven Development is enforced through [godog](https://github.com/cucumber/godog). You can find the specification of the implemented Features at in the [acceptance/features folder](./acceptance/features/). -They rely on [kind](https://kind.sigs.k8s.io/) and can be executed by just running the following commands: +## Try -```bash -make -C acceptance prepare # required just the first time -make -C acceptance test -``` +The easiest way to try this component locally is by using the `make prepare` target in `acceptance/test/dumb-proxy` or `acceptance/test/smart-proxy`. +This command will build the image, create the Kind cluster, load the image in it, and deploy all needed components. -* `prepare` will build the image, create the Kind cluster, load the image in it, and deploy all needed components. -* `test` will run the tests on the provisioned infrastructure +Please take a look at the [Acceptance Tests README](./acceptance/README.md) for more information on the two setups and how to access the namespace-lister once deployed. ### Proxy diff --git a/acceptance/Makefile b/acceptance/Makefile deleted file mode 100644 index f77c44f..0000000 --- a/acceptance/Makefile +++ /dev/null @@ -1,92 +0,0 @@ -.PHONY: prepare -prepare: image-build kind-create load-image deploy-test-infra deploy-namespace-lister deploy-test-proxy - @: - -.PHONY: update-namespace-lister -update-namespace-lister: image-build load-image - kubectl rollout restart deployment namespace-lister -n namespace-lister - kubectl rollout status deployment -n namespace-lister namespace-lister - -.PHONY: image-build -image-build: - $(MAKE) -C .. image-build - -.PHONY: kind-create -kind-create: - kind create cluster --name namespace-lister --config kind-config.yaml - -.PHONY: load-image -load-image: - kind load docker-image --name namespace-lister namespace-lister:latest - -.PHONY: deploy-test-infra -deploy-test-infra: - kubectl apply -k ./dependencies/cert-manager/ - sleep 5 - kubectl wait --for=condition=Ready --timeout=300s -l 'app.kubernetes.io/instance=cert-manager' -n cert-manager pod - kubectl apply -k ./dependencies/cluster-issuer/ - -.PHONY: deploy-test-proxy -deploy-test-proxy: - kubectl apply -k ./config/proxy/ - -.PHONY: deploy-namespace-lister -deploy-namespace-lister: - cd ../config/ && \ - kustomize edit set namespace namespace-lister - kubectl apply -k ../config/ - -.PHONY: create-test-identity -create-test-identity: - kubectl apply -k ./config/acceptance-tests/ - -.PHONY: export-test-identity-kubeconfig -export-test-identity-kubeconfig: - kind get kubeconfig --name namespace-lister > /tmp/namespace-lister-acceptance-tests-user.kcfg - yq -i '.users[0].user={"token": "'$$(kubectl get secret acceptance-tests-user -n acceptance-tests -o jsonpath='{.data.token}' | base64 -d )'"}' /tmp/namespace-lister-acceptance-tests-user.kcfg - -.PHONY: vet -vet: - go vet ./... - -.PHONY: clean -clean: - kubectl delete namespace -l namespace-lister/scope=acceptance-tests - -.PHONY: wip -wip: vet clean create-test-identity export-test-identity-kubeconfig - kubectl rollout status deployment -n namespace-lister namespace-lister - kubectl rollout status deployment -n namespace-lister namespace-lister-proxy - KUBECONFIG=/tmp/namespace-lister-acceptance-tests-user.kcfg \ - KONFLUX_ADDRESS=https://localhost:10443 \ - E2E_USE_INSECURE_TLS=true \ - go test ./... -v --godog.tags=wip --godog.concurrency=1 - -.PHONY: test -test: vet clean create-test-identity export-test-identity-kubeconfig - kubectl rollout status deployment -n namespace-lister namespace-lister - kubectl rollout status deployment -n namespace-lister namespace-lister-proxy - KUBECONFIG=/tmp/namespace-lister-acceptance-tests-user.kcfg \ - KONFLUX_ADDRESS=https://localhost:10443 \ - E2E_USE_INSECURE_TLS=true \ - go test ./... -v --godog.tags=~skip --godog.concurrency=1 - - -ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) -LOCALBIN := $(ROOT_DIR)/bin - -OUTDIR := $(ROOT_DIR)/out - -GO ?= go - -GOLANG_CI ?= $(GO) run -modfile $(shell dirname $(ROOT_DIR))/hack/tools/golang-ci/go.mod github.com/golangci/golangci-lint/cmd/golangci-lint - -## Local Folders -$(LOCALBIN): - mkdir $(LOCALBIN) -$(OUTDIR): - @mkdir $(OUTDIR) - -.PHONY: lint -lint: ## Run go linter. - $(GOLANG_CI) run ./... diff --git a/acceptance/README.md b/acceptance/README.md new file mode 100644 index 0000000..a2d3ca0 --- /dev/null +++ b/acceptance/README.md @@ -0,0 +1,22 @@ +# Acceptance Tests + +Behavior-Driven Development is enforced through [godog](https://github.com/cucumber/godog). + +These tests has builtin support to run on [kind](https://kind.sigs.k8s.io/). + +## Setups + +The Namespace-Lister is usually installed behind a Proxy. +The Namespace-Lister can be configured to delegate authentication to the Proxy. +In this case we speak of a `Smart Proxy`. + +Alternatively, the request is authenticate against the APIServer's TokenReview API. +In this case we speak of a `Dumb Proxy`. + +We support test cases for both these setups. +You find the `Smart Proxy`'s tests at [./test/smart-proxy/] and the `Dumb Proxy`'s tests at [./test/dumb-proxy/]. + +To create the cluster, install the Namespace-Lister, and configure the Proxy you can use the `make prepare` command. + +To execute the tests, you can use the `make test` command. + diff --git a/acceptance/config/acceptance-tests/namespace.yaml b/acceptance/config/acceptance-tests/namespace.yaml index 9b1c8b6..5cb531f 100644 --- a/acceptance/config/acceptance-tests/namespace.yaml +++ b/acceptance/config/acceptance-tests/namespace.yaml @@ -2,3 +2,5 @@ apiVersion: v1 kind: Namespace metadata: name: acceptance-tests + labels: + namespace-lister/scope: acceptance-tests diff --git a/acceptance/features/list.feature b/acceptance/features/list.feature deleted file mode 100644 index 34f11f6..0000000 --- a/acceptance/features/list.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: List Namespaces - - Scenario: user list namespace - Given user has access to "10" namespaces - Then the user can retrieve only the namespaces they have access to diff --git a/acceptance/features/read.feature b/acceptance/features/read.feature deleted file mode 100644 index f8cbd71..0000000 --- a/acceptance/features/read.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: List Namespaces - - Scenario: user get namespace - Given user has access to a namespace - Then the user can retrieve the namespace diff --git a/acceptance/go.mod b/acceptance/go.mod index 5ca3ebe..ecd3c64 100644 --- a/acceptance/go.mod +++ b/acceptance/go.mod @@ -3,12 +3,12 @@ module github.com/konflux-ci/namespace-lister/acceptance go 1.22.2 require ( - github.com/cucumber/godog v0.14.1 + github.com/cucumber/godog v0.15.0 github.com/spf13/pflag v1.0.5 - k8s.io/api v0.31.1 - k8s.io/apimachinery v0.31.1 - k8s.io/client-go v0.31.0 - sigs.k8s.io/controller-runtime v0.19.1 + k8s.io/api v0.31.3 + k8s.io/apimachinery v0.31.3 + k8s.io/client-go v0.31.3 + sigs.k8s.io/controller-runtime v0.19.2 ) require ( @@ -44,8 +44,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.20.1 // indirect - github.com/onsi/gomega v1.35.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -54,15 +52,15 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.30.0 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/acceptance/go.sum b/acceptance/go.sum index 8605e93..e2c9b74 100644 --- a/acceptance/go.sum +++ b/acceptance/go.sum @@ -6,8 +6,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= -github.com/cucumber/godog v0.14.1 h1:HGZhcOyyfaKclHjJ+r/q93iaTJZLKYW6Tv3HkmUE6+M= -github.com/cucumber/godog v0.14.1/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= +github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= @@ -55,8 +55,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -95,10 +95,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -141,16 +141,16 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -159,30 +159,30 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -196,22 +196,22 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= -k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= +k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= -k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= -k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= -k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= +k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= -sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-runtime v0.19.2 h1:3sPrF58XQEPzbE8T81TN6selQIMGbtYwuaJ6eDssDF8= +sigs.k8s.io/controller-runtime v0.19.2/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/acceptance/hook_test.go b/acceptance/hook_test.go deleted file mode 100644 index 9f25302..0000000 --- a/acceptance/hook_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package acceptance - -import ( - "context" - - "github.com/cucumber/godog" - - tcontext "github.com/konflux-ci/namespace-lister/acceptance/pkg/context" -) - -func InjectHooks(ctx *godog.ScenarioContext) { - ctx.Before(injectRun) -} - -func injectRun(ctx context.Context, sc *godog.Scenario) (context.Context, error) { - return tcontext.WithRunId(ctx, sc.Id), nil -} diff --git a/acceptance/make/common.mk b/acceptance/make/common.mk new file mode 100644 index 0000000..407de28 --- /dev/null +++ b/acceptance/make/common.mk @@ -0,0 +1,71 @@ +KIND_CLUSTER_NAME ?= namespace-lister-acceptance-tests +IMG ?= namespace-lister:latest + +ROOT_DIR ?= $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +LOCALBIN ?= $(ROOT_DIR)/bin + +OUT_DIR ?= $(ROOT_DIR)/out + +GO ?= go + +GOLANG_CI ?= $(GO) run -modfile $(shell dirname $(ROOT_DIR))/hack/tools/golang-ci/go.mod github.com/golangci/golangci-lint/cmd/golangci-lint + +KUBECTL ?= kubectl +KIND ?= kind +KUBECONFIG_ATSA ?= /tmp/namespace-lister-acceptance-tests-user.kcfg + +## Local Folders +$(LOCALBIN): + mkdir $(LOCALBIN) +$(OUT_DIR): + @mkdir $(OUT_DIR) + +.PHONY: lint +lint: ## Run go linter. + $(GOLANG_CI) run ./... + +.PHONY: image-build +image-build: + $(MAKE) -C $(ROOT_DIR)/.. image-build + +.PHONY: kind-create +kind-create: + $(KIND) create cluster --name "$(KIND_CLUSTER_NAME)" --config kind-config.yaml + +.PHONY: kind-load-image +kind-load-image: + $(IMAGE_BUILDER) save $(IMG) --quiet | \ + $(KIND) load image-archive --name $(KIND_CLUSTER_NAME) /dev/stdin + +.PHONY: update-namespace-lister +update-namespace-lister: image-build load-image + $(KUBECTL) rollout restart deployment namespace-lister -n namespace-lister + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister + +.PHONY: deploy-test-infra +deploy-test-infra: + $(KUBECTL) apply -k $(ROOT_DIR)/dependencies/cert-manager/ + $(KUBECTL) rollout status \ + --timeout=300s \ + -l 'app.kubernetes.io/instance=cert-manager' \ + -n cert-manager deployment + $(KUBECTL) apply -k $(ROOT_DIR)/dependencies/cluster-issuer/ + +.PHONY: create-test-identity +create-test-identity: + $(KUBECTL) apply -k $(ROOT_DIR)/config/acceptance-tests/ + +.PHONY: export-test-identity-kubeconfig +export-test-identity-kubeconfig: + $(KIND) get kubeconfig --name $(KIND_CLUSTER_NAME) | \ + yq '.users[0].user={"token": "'$$($(KUBECTL) get secret acceptance-tests-user -n acceptance-tests -o jsonpath='{.data.token}' | base64 -d )'"}' >| \ + $(KUBECONFIG_ATSA) + +.PHONY: vet +vet: + go vet ./... + +.PHONY: clean +clean: + $(KUBECTL) delete namespace -l 'namespace-lister/scope=acceptance-tests' + diff --git a/acceptance/pkg/context/context.go b/acceptance/pkg/context/context.go index 46dba18..30c4d5b 100644 --- a/acceptance/pkg/context/context.go +++ b/acceptance/pkg/context/context.go @@ -4,15 +4,36 @@ import ( "context" corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) type ContextKey string const ( - ContextKeyNamespaces ContextKey = "namespaces" - ContextKeyRunId ContextKey = "run-id" + ContextKeyNamespaces ContextKey = "namespaces" + ContextKeyRunId ContextKey = "run-id" + ContextKeyUserInfo ContextKey = "user-info" + ContextKeyBuildUserClient ContextKey = "build-user-client" ) +type BuildUserClientFunc func(context.Context) (client.Client, error) + +func WithBuildUserClientFunc(ctx context.Context, builder BuildUserClientFunc) context.Context { + return into(ctx, ContextKeyBuildUserClient, builder) +} + +func InvokeBuildUserClientFunc(ctx context.Context) (client.Client, error) { + return get[BuildUserClientFunc](ctx, ContextKeyBuildUserClient)(ctx) +} + +func WithUser(ctx context.Context, userInfo UserInfo) context.Context { + return into(ctx, ContextKeyUserInfo, userInfo) +} + +func User(ctx context.Context) UserInfo { + return get[UserInfo](ctx, ContextKeyUserInfo) +} + func WithNamespaces(ctx context.Context, namespaces []corev1.Namespace) context.Context { return into(ctx, ContextKeyNamespaces, namespaces) } @@ -35,5 +56,10 @@ func into[T any](ctx context.Context, key ContextKey, value T) context.Context { } func get[T any](ctx context.Context, key ContextKey) T { - return ctx.Value(key).(T) + if v, ok := ctx.Value(key).(T); ok { + return v + } + + var t T + return t } diff --git a/acceptance/pkg/context/user_info.go b/acceptance/pkg/context/user_info.go new file mode 100644 index 0000000..3f3175e --- /dev/null +++ b/acceptance/pkg/context/user_info.go @@ -0,0 +1,44 @@ +package context + +import ( + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +type UserInfo struct { + Token string + Namespace string + Name string + APIGroup string + Kind string +} + +func UserInfoFromServiceAccount(sa corev1.ServiceAccount, tkn *authenticationv1.TokenRequest) UserInfo { + return UserInfo{ + Namespace: sa.Namespace, + Name: sa.Name, + Kind: "ServiceAccount", + APIGroup: "", + Token: tkn.Status.Token, + } +} + +func UserInfoFromUsername(username string) UserInfo { + return UserInfo{ + Namespace: "", + Name: username, + Kind: "User", + APIGroup: rbacv1.GroupName, + Token: "", + } +} + +func (u *UserInfo) AsSubject() rbacv1.Subject { + return rbacv1.Subject{ + Namespace: u.Namespace, + Name: u.Name, + APIGroup: u.APIGroup, + Kind: u.Kind, + } +} diff --git a/acceptance/pkg/rest/client.go b/acceptance/pkg/rest/client.go index 8fa02e4..ef7e555 100644 --- a/acceptance/pkg/rest/client.go +++ b/acceptance/pkg/rest/client.go @@ -42,10 +42,16 @@ func BuildDefaultHostClient() (client.Client, error) { } func BuildClient(cfg *rest.Config) (client.Client, error) { - scheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + return BuildClientWithOptions(cfg, client.Options{}) +} + +func BuildClientWithOptions(cfg *rest.Config, options client.Options) (client.Client, error) { + if options.Scheme == nil { + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + } - return client.New(cfg, client.Options{Scheme: scheme}) + return client.New(cfg, options) } // BuildDefaultRESTMapper builds a RESTMapper from the default client configuration. diff --git a/acceptance/pkg/suite/env.go b/acceptance/pkg/suite/env.go new file mode 100644 index 0000000..f65a7bc --- /dev/null +++ b/acceptance/pkg/suite/env.go @@ -0,0 +1,12 @@ +package suite + +import ( + "cmp" + "os" +) + +const EnvKonfluxAddress string = "KONFLUX_ADDRESS" + +func EnvKonfluxAddressOrDefault(address string) string { + return cmp.Or(os.Getenv(EnvKonfluxAddress), address) +} diff --git a/acceptance/pkg/suite/hooks.go b/acceptance/pkg/suite/hooks.go new file mode 100644 index 0000000..2550a74 --- /dev/null +++ b/acceptance/pkg/suite/hooks.go @@ -0,0 +1,56 @@ +package suite + +import ( + "context" + "fmt" + + "github.com/cucumber/godog" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + tcontext "github.com/konflux-ci/namespace-lister/acceptance/pkg/context" + "github.com/konflux-ci/namespace-lister/acceptance/pkg/rest" +) + +func InjectBaseHooks(ctx *godog.ScenarioContext) { + ctx.Before(InjectRunId) + ctx.Before(PrepareTestRunServiceAccount) +} + +func InjectRunId(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + return tcontext.WithRunId(ctx, sc.Id), nil +} + +func PrepareTestRunServiceAccount(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + cli, err := rest.BuildDefaultHostClient() + if err != nil { + return ctx, err + } + + // create serviceaccount + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("user-%s", sc.Id), + Namespace: "acceptance-tests", + Labels: map[string]string{ + "namespace-lister/scope": "acceptance-tests", + "namespace-lister/test-run": sc.Id, + }, + }, + } + if err := cli.Create(ctx, sa); err != nil && !errors.IsAlreadyExists(err) { + return ctx, err + } + + // create a token for authenticating as the service account + tkn := &authenticationv1.TokenRequest{} + if err := cli.SubResource("token").Create(ctx, sa, tkn); err != nil { + return ctx, err + } + + // store auth info in context for future use + ui := tcontext.UserInfoFromServiceAccount(*sa, tkn) + return tcontext.WithUser(ctx, ui), nil +} diff --git a/acceptance/step_test.go b/acceptance/pkg/suite/steps.go similarity index 69% rename from acceptance/step_test.go rename to acceptance/pkg/suite/steps.go index 537cc22..561bfc0 100644 --- a/acceptance/step_test.go +++ b/acceptance/pkg/suite/steps.go @@ -1,54 +1,53 @@ -package acceptance +package suite import ( - "cmp" "context" "fmt" - "os" "slices" "github.com/cucumber/godog" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" tcontext "github.com/konflux-ci/namespace-lister/acceptance/pkg/context" - "github.com/konflux-ci/namespace-lister/acceptance/pkg/rest" + arest "github.com/konflux-ci/namespace-lister/acceptance/pkg/rest" ) func InjectSteps(ctx *godog.ScenarioContext) { //read - ctx.Step(`^user has access to a namespace$`, + ctx.Step(`^ServiceAccount has access to a namespace$`, + func(ctx context.Context) (context.Context, error) { return UserInfoHasAccessToNNamespaces(ctx, 1) }) + ctx.Step(`^User has access to a namespace$`, func(ctx context.Context) (context.Context, error) { return UserHasAccessToNNamespaces(ctx, 1) }) - ctx.Step(`^the user can retrieve the namespace$`, TheUserCanRetrieveTheNamespace) + ctx.Step(`^the ServiceAccount can retrieve the namespace$`, TheUserCanRetrieveTheNamespace) // list - ctx.Step(`^user has access to "([^"]*)" namespaces$`, UserHasAccessToNNamespaces) - ctx.Step(`^the user can retrieve only the namespaces they have access to$`, TheUserCanRetrieveOnlyTheNamespacesTheyHaveAccessTo) + ctx.Step(`^ServiceAccount has access to "([^"]*)" namespaces$`, UserInfoHasAccessToNNamespaces) + ctx.Step(`^User has access to "([^"]*)" namespaces$`, UserHasAccessToNNamespaces) + ctx.Step(`^the ServiceAccount can retrieve only the namespaces they have access to$`, TheUserCanRetrieveOnlyTheNamespacesTheyHaveAccessTo) + ctx.Step(`^the User can retrieve only the namespaces they have access to$`, TheUserCanRetrieveOnlyTheNamespacesTheyHaveAccessTo) } func UserHasAccessToNNamespaces(ctx context.Context, number int) (context.Context, error) { + runId := tcontext.RunId(ctx) + username := fmt.Sprintf("user-%s", runId) + userId := tcontext.UserInfoFromUsername(username) + ctx = tcontext.WithUser(ctx, userId) + return UserInfoHasAccessToNNamespaces(ctx, number) +} + +func UserInfoHasAccessToNNamespaces(ctx context.Context, number int) (context.Context, error) { run := tcontext.RunId(ctx) + user := tcontext.User(ctx) - cli, err := rest.BuildDefaultHostClient() + cli, err := arest.BuildDefaultHostClient() if err != nil { return ctx, err } - // create serviceaccount - if err := cli.Create(ctx, &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "user", - Namespace: "default", - }, - }); err != nil && !errors.IsAlreadyExists(err) { - return ctx, err - } - // create namespaces nn := []corev1.Namespace{} for i := range number { @@ -75,13 +74,7 @@ func UserHasAccessToNNamespaces(ctx context.Context, number int) (context.Contex Name: "namespace-get", APIGroup: rbacv1.GroupName, }, - Subjects: []rbacv1.Subject{ - { - Kind: "User", - APIGroup: rbacv1.GroupName, - Name: "user", - }, - }, + Subjects: []rbacv1.Subject{user.AsSubject()}, }); err != nil { return ctx, err } @@ -93,7 +86,7 @@ func UserHasAccessToNNamespaces(ctx context.Context, number int) (context.Contex } func TheUserCanRetrieveOnlyTheNamespacesTheyHaveAccessTo(ctx context.Context) (context.Context, error) { - cli, err := buildUserClient() + cli, err := tcontext.InvokeBuildUserClientFunc(ctx) if err != nil { return ctx, err } @@ -122,7 +115,7 @@ func TheUserCanRetrieveOnlyTheNamespacesTheyHaveAccessTo(ctx context.Context) (c func TheUserCanRetrieveTheNamespace(ctx context.Context) (context.Context, error) { run := tcontext.RunId(ctx) - cli, err := buildUserClient() + cli, err := tcontext.InvokeBuildUserClientFunc(ctx) if err != nil { return ctx, err } @@ -144,14 +137,3 @@ func TheUserCanRetrieveTheNamespace(ctx context.Context) (context.Context, error return ctx, nil } - -func buildUserClient() (client.Client, error) { - // build impersonating client - cfg, err := rest.NewDefaultClientConfig() - if err != nil { - return nil, err - } - cfg.Impersonate.UserName = "user" - cfg.Host = cmp.Or(os.Getenv("KONFLUX_ADDRESS"), "https://localhost:10443") - return rest.BuildClient(cfg) -} diff --git a/acceptance/test/dumb-proxy/.gitignore b/acceptance/test/dumb-proxy/.gitignore new file mode 100644 index 0000000..89f9ac0 --- /dev/null +++ b/acceptance/test/dumb-proxy/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/acceptance/test/dumb-proxy/Makefile b/acceptance/test/dumb-proxy/Makefile new file mode 100644 index 0000000..780d208 --- /dev/null +++ b/acceptance/test/dumb-proxy/Makefile @@ -0,0 +1,50 @@ +CWD ?= $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +ROOT_DIR := $(CWD)/../.. +OUT_DIR := $(CWD)/out +KIND_CLUSTER_NAME := namespace-lister-dumb-proxy +KUBECONFIG_ATSA := $(OUT_DIR)/namespace-lister-acceptance-tests-user.kcfg +KUBECONFIG_ADMIN := $(OUT_DIR)/namespace-lister-admin.kcfg +KONFLUX_ADDRESS := https://localhost:10443 + +include ../../make/*.mk + +export KUBECONFIG=$(KUBECONFIG_ADMIN) + +.PHONY: prepare +prepare: image-build kind-create kind-load-image deploy-test-infra deploy-namespace-lister deploy-test-proxy + @: + +.PHONY: deploy-test-proxy +deploy-test-proxy: + $(KUBECTL) apply -k $(CWD)/config/proxy/ + +.PHONY: deploy-namespace-lister +deploy-namespace-lister: $(OUT_DIR) + -rm -r $(OUT_DIR)/config + mkdir -p $(OUT_DIR)/config + ( \ + cd $(OUT_DIR)/config && \ + kustomize init && \ + kustomize edit add base "../../../../../config" && \ + kustomize edit set namespace namespace-lister && \ + kustomize edit set image "namespace-lister:latest=$(IMG)" && \ + kustomize build | $(KUBECTL) apply -f - ; \ + ) + +.PHONY: wip +wip: vet clean create-test-identity export-test-identity-kubeconfig + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister-proxy + KUBECONFIG=$(KUBECONFIG_ATSA) \ + KONFLUX_ADDRESS=$(KONFLUX_ADDRESS) \ + E2E_USE_INSECURE_TLS=true \ + go test $(CWD)/... -v --godog.tags=wip --godog.concurrency=1 + +.PHONY: test +test: vet clean create-test-identity export-test-identity-kubeconfig + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister-proxy + KUBECONFIG=$(KUBECONFIG_ATSA) \ + KONFLUX_ADDRESS=$(KONFLUX_ADDRESS) \ + E2E_USE_INSECURE_TLS=true \ + go test $(CWD)/... -v --godog.tags=~skip --godog.concurrency=1 diff --git a/acceptance/test/dumb-proxy/README.md b/acceptance/test/dumb-proxy/README.md new file mode 100644 index 0000000..1ab54ec --- /dev/null +++ b/acceptance/test/dumb-proxy/README.md @@ -0,0 +1,5 @@ +# Dumb Proxy + +In this setup the proxy is not implementing any authentication logic. + +It forwards `/api/v1/namespaces` and `/api/v1/namespace/` to the Namespace-Lister, whereas all the others to the Kubernetes APIServer. diff --git a/acceptance/config/proxy/certificate.yaml b/acceptance/test/dumb-proxy/config/proxy/certificate.yaml similarity index 100% rename from acceptance/config/proxy/certificate.yaml rename to acceptance/test/dumb-proxy/config/proxy/certificate.yaml diff --git a/acceptance/config/proxy/kustomization.yaml b/acceptance/test/dumb-proxy/config/proxy/kustomization.yaml similarity index 100% rename from acceptance/config/proxy/kustomization.yaml rename to acceptance/test/dumb-proxy/config/proxy/kustomization.yaml diff --git a/acceptance/config/proxy/nginx.conf b/acceptance/test/dumb-proxy/config/proxy/nginx.conf similarity index 97% rename from acceptance/config/proxy/nginx.conf rename to acceptance/test/dumb-proxy/config/proxy/nginx.conf index 4946b12..ff8ac71 100644 --- a/acceptance/config/proxy/nginx.conf +++ b/acceptance/test/dumb-proxy/config/proxy/nginx.conf @@ -15,6 +15,7 @@ http { log_format upstreamlog '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" $request_time ' + '"$req_headers" ' '<"$request_body" >"$resp_body"'; access_log /dev/stderr upstreamlog; @@ -41,7 +42,6 @@ http { server { listen 9443 ssl; - ignore_invalid_headers off; ssl_certificate /mnt/tls.crt; ssl_certificate_key /mnt/tls.key; server_name _; @@ -64,7 +64,7 @@ http { } location /health { - # Used for liveness probes + # used for liveness probes return 200; } diff --git a/acceptance/config/proxy/proxy.yaml b/acceptance/test/dumb-proxy/config/proxy/proxy.yaml similarity index 96% rename from acceptance/config/proxy/proxy.yaml rename to acceptance/test/dumb-proxy/config/proxy/proxy.yaml index 207d553..371c2bd 100644 --- a/acceptance/config/proxy/proxy.yaml +++ b/acceptance/test/dumb-proxy/config/proxy/proxy.yaml @@ -36,8 +36,8 @@ spec: path: /health port: 9443 scheme: HTTPS - initialDelaySeconds: 30 - periodSeconds: 60 + initialDelaySeconds: 3 + periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 readinessProbe: @@ -46,8 +46,8 @@ spec: path: /health port: 9443 scheme: HTTPS - initialDelaySeconds: 30 - periodSeconds: 30 + initialDelaySeconds: 3 + periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 ports: diff --git a/acceptance/config/proxy/rbac.yaml b/acceptance/test/dumb-proxy/config/proxy/rbac.yaml similarity index 100% rename from acceptance/config/proxy/rbac.yaml rename to acceptance/test/dumb-proxy/config/proxy/rbac.yaml diff --git a/acceptance/test/dumb-proxy/features/list.feature b/acceptance/test/dumb-proxy/features/list.feature new file mode 100644 index 0000000..531606b --- /dev/null +++ b/acceptance/test/dumb-proxy/features/list.feature @@ -0,0 +1,5 @@ +Feature: List Namespaces + + Scenario: ServiceAccount list namespace + Given ServiceAccount has access to "10" namespaces + Then the ServiceAccount can retrieve only the namespaces they have access to diff --git a/acceptance/test/dumb-proxy/features/read.feature b/acceptance/test/dumb-proxy/features/read.feature new file mode 100644 index 0000000..69a0ba0 --- /dev/null +++ b/acceptance/test/dumb-proxy/features/read.feature @@ -0,0 +1,5 @@ +Feature: List Namespaces + + Scenario: ServiceAccount get namespace + Given ServiceAccount has access to a namespace + Then the ServiceAccount can retrieve the namespace diff --git a/acceptance/test/dumb-proxy/hook_test.go b/acceptance/test/dumb-proxy/hook_test.go new file mode 100644 index 0000000..b9bf5de --- /dev/null +++ b/acceptance/test/dumb-proxy/hook_test.go @@ -0,0 +1,37 @@ +package acceptance + +import ( + "context" + + "github.com/cucumber/godog" + "sigs.k8s.io/controller-runtime/pkg/client" + + tcontext "github.com/konflux-ci/namespace-lister/acceptance/pkg/context" + arest "github.com/konflux-ci/namespace-lister/acceptance/pkg/rest" + "github.com/konflux-ci/namespace-lister/acceptance/pkg/suite" +) + +const defaultTestAddress string = "https://localhost:10443" + +func InjectHooks(ctx *godog.ScenarioContext) { + suite.InjectBaseHooks(ctx) + + ctx.Before(injectBuildUserClient) +} + +func injectBuildUserClient(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + return tcontext.WithBuildUserClientFunc(ctx, buildUserClientWithTokenReview), nil +} + +func buildUserClientWithTokenReview(ctx context.Context) (client.Client, error) { + // build client with bearer token + cfg, err := arest.NewDefaultClientConfig() + if err != nil { + return nil, err + } + + cfg.BearerToken = tcontext.User(ctx).Token + cfg.Host = suite.EnvKonfluxAddressOrDefault(defaultTestAddress) + + return arest.BuildClient(cfg) +} diff --git a/acceptance/kind-config.yaml b/acceptance/test/dumb-proxy/kind-config.yaml similarity index 84% rename from acceptance/kind-config.yaml rename to acceptance/test/dumb-proxy/kind-config.yaml index 2faf0e7..13e080d 100644 --- a/acceptance/kind-config.yaml +++ b/acceptance/test/dumb-proxy/kind-config.yaml @@ -1,8 +1,6 @@ --- kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 -featureGates: - "StructuredAuthenticationConfiguration": true nodes: - role: control-plane kubeadmConfigPatches: diff --git a/acceptance/test/dumb-proxy/step_test.go b/acceptance/test/dumb-proxy/step_test.go new file mode 100644 index 0000000..9be69e3 --- /dev/null +++ b/acceptance/test/dumb-proxy/step_test.go @@ -0,0 +1,11 @@ +package acceptance + +import ( + "github.com/cucumber/godog" + + "github.com/konflux-ci/namespace-lister/acceptance/pkg/suite" +) + +func InjectSteps(ctx *godog.ScenarioContext) { + suite.InjectSteps(ctx) +} diff --git a/acceptance/suite_test.go b/acceptance/test/dumb-proxy/suite_test.go similarity index 100% rename from acceptance/suite_test.go rename to acceptance/test/dumb-proxy/suite_test.go diff --git a/acceptance/test/smart-proxy/.gitignore b/acceptance/test/smart-proxy/.gitignore new file mode 100644 index 0000000..89f9ac0 --- /dev/null +++ b/acceptance/test/smart-proxy/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/acceptance/test/smart-proxy/Makefile b/acceptance/test/smart-proxy/Makefile new file mode 100644 index 0000000..2736078 --- /dev/null +++ b/acceptance/test/smart-proxy/Makefile @@ -0,0 +1,58 @@ +CWD ?= $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +ROOT_DIR := $(CWD)/../.. +OUT_DIR := $(CWD)/out +KIND_CLUSTER_NAME := namespace-lister-smart-proxy +KUBECONFIG_ATSA := $(OUT_DIR)/namespace-lister-acceptance-tests-user.kcfg +KUBECONFIG_ADMIN := $(OUT_DIR)/namespace-lister-admin.kcfg +KONFLUX_ADDRESS := https://localhost:11443 + +include ../../make/*.mk + +export KUBECONFIG=$(KUBECONFIG_ADMIN) + +.PHONY: prepare +prepare: image-build kind-create kind-load-image deploy-test-infra deploy-namespace-lister deploy-test-proxy + @: + +.PHONY: deploy-test-proxy +deploy-test-proxy: + $(KUBECTL) apply -k ./config/proxy-auth/ + +.PHONY: deploy-namespace-lister +deploy-namespace-lister: $(OUT_DIR) + -rm -r $(OUT_DIR)/config + mkdir -p $(OUT_DIR)/config + ( \ + cd $(OUT_DIR)/config && \ + kustomize init && \ + kustomize edit add base "../../../../../config" && \ + cp "$(ROOT_DIR)/../config/patches/with-header-auth.yaml" . && \ + kustomize edit add patch \ + --path "with-header-auth.yaml" \ + --group "apps" \ + --kind "Deployment" \ + --name "namespace-lister" \ + --version "v1" && \ + kustomize edit set namespace namespace-lister && \ + kustomize edit set image "namespace-lister:latest=$(IMG)" && \ + kustomize build | $(KUBECTL) apply -f - ; \ + ) + +.PHONY: wip +wip: vet clean create-test-identity export-test-identity-kubeconfig + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister-proxy-auth + KUBECONFIG=$(KUBECONFIG_ATSA) \ + KONFLUX_ADDRESS=$(KONFLUX_ADDRESS) \ + E2E_USE_INSECURE_TLS=true \ + go test $(CWD)/... -v --godog.tags=wip --godog.concurrency=1 + +.PHONY: test +test: vet clean create-test-identity export-test-identity-kubeconfig + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister + $(KUBECTL) rollout status deployment -n namespace-lister namespace-lister-proxy-auth + KUBECONFIG=$(KUBECONFIG_ATSA) \ + KONFLUX_ADDRESS=$(KONFLUX_ADDRESS) \ + E2E_USE_INSECURE_TLS=true \ + go test $(CWD)/... -v --godog.tags=~skip --godog.concurrency=1 + diff --git a/acceptance/test/smart-proxy/README.md b/acceptance/test/smart-proxy/README.md new file mode 100644 index 0000000..bc04ffc --- /dev/null +++ b/acceptance/test/smart-proxy/README.md @@ -0,0 +1,11 @@ +# Smart Proxy + +In this setup the proxy is supposed to implement some sort of authentication logic. + +It forwards `/api/v1/namespaces` and `/api/v1/namespace/` to the Namespace-Lister, whereas all the others to the Kubernetes APIServer. + +For each request it will inject the Bearer Token for authenticating as a Cluster Admin ServiceAccount and set the `Impersonate-User` header to the authenticated user. + +For simplicity, the User is already expected to be in the `Impersonate-User` header. +So, unauthenticated requests that can impersoante anyone are supported. +Be mindful about it. diff --git a/acceptance/test/smart-proxy/config/proxy-auth/certificate.yaml b/acceptance/test/smart-proxy/config/proxy-auth/certificate.yaml new file mode 100644 index 0000000..56ee57a --- /dev/null +++ b/acceptance/test/smart-proxy/config/proxy-auth/certificate.yaml @@ -0,0 +1,11 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: serving-cert +spec: + dnsNames: + - localhost + issuerRef: + kind: ClusterIssuer + name: self-signed-cluster-issuer + secretName: serving-cert diff --git a/acceptance/test/smart-proxy/config/proxy-auth/kustomization.yaml b/acceptance/test/smart-proxy/config/proxy-auth/kustomization.yaml new file mode 100644 index 0000000..ccef12b --- /dev/null +++ b/acceptance/test/smart-proxy/config/proxy-auth/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- certificate.yaml +- rbac.yaml +- proxy.yaml +namespace: namespace-lister +configMapGenerator: +- name: namespace-lister-proxy-auth + files: + - nginx.conf diff --git a/acceptance/test/smart-proxy/config/proxy-auth/nginx.conf b/acceptance/test/smart-proxy/config/proxy-auth/nginx.conf new file mode 100644 index 0000000..fbe16d4 --- /dev/null +++ b/acceptance/test/smart-proxy/config/proxy-auth/nginx.conf @@ -0,0 +1,100 @@ +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + # log_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name $host to: $proxy_host $upstream_addr: $request $status upstream_response_time $upstream_response_time msec $msec request_time $request_time'; + + log_format upstreamlog '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" $request_time ' + '"$req_headers" ' + '<"$request_body" >"$resp_body"'; + + access_log /dev/stderr upstreamlog; + error_log /dev/stderr; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 4096; + + default_type application/octet-stream; + + client_body_temp_path /var/run/openresty/nginx-client-body; + proxy_temp_path /var/run/openresty/nginx-proxy; + fastcgi_temp_path /var/run/openresty/nginx-fastcgi; + uwsgi_temp_path /var/run/openresty/nginx-uwsgi; + scgi_temp_path /var/run/openresty/nginx-scgi; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + server { + listen 9443 ssl; + ssl_certificate /mnt/tls.crt; + ssl_certificate_key /mnt/tls.key; + server_name _; + root /opt/app-root/src; + + location = /404.html { + } + + location ~* /api/v1/namespaces(/?)$ { + # namespace-lister endpoint + rewrite ^/(.*)/$ /$1 permanent; + proxy_pass http://namespace-lister.namespace-lister.svc.cluster.local:12000; + proxy_read_timeout 1m; + } + + location / { + # Kube-API + proxy_pass https://kubernetes.default.svc/; + proxy_read_timeout 1m; + include /mnt/nginx-generated-config/bearer.conf; + } + + location /health { + # used for liveness probes + return 200; + } + + lua_need_request_body on; + + set $resp_body ""; + set $req_body ""; + set $req_headers ""; + + client_body_buffer_size 16k; + client_max_body_size 16k; + + rewrite_by_lua_block { + local req_headers = "Headers: "; + ngx.var.req_body = ngx.req.get_body_data(); + local h, err = ngx.req.get_headers() + for k, v in pairs(h) do + req_headers = req_headers .. k .. ": " .. v .. "\n"; + end + + ngx.var.req_headers = req_headers; + } + + body_filter_by_lua ' + local resp_body = string.sub(ngx.arg[1], 1, 5000) + ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body + if ngx.arg[2] then + ngx.var.resp_body = ngx.ctx.buffered + end + '; + } +} diff --git a/acceptance/test/smart-proxy/config/proxy-auth/proxy.yaml b/acceptance/test/smart-proxy/config/proxy-auth/proxy.yaml new file mode 100644 index 0000000..e6e8b71 --- /dev/null +++ b/acceptance/test/smart-proxy/config/proxy-auth/proxy.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: namespace-lister-proxy-auth + name: namespace-lister-proxy-auth +spec: + replicas: 1 + selector: + matchLabels: + app: namespace-lister-proxy-auth + minReadySeconds: 30 + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 0 + template: + metadata: + labels: + app: namespace-lister-proxy-auth + spec: + serviceAccountName: namespace-lister-proxy-auth + initContainers: + - name: add-sva-token-to-nginx-config + image: registry.access.redhat.com/ubi9/ubi@sha256:66233eebd72bb5baa25190d4f55e1dc3fff3a9b77186c1f91a0abdb274452072 + command: + - sh + - -c + - | + set -e + token=$(cat /mnt/api-token/token) + token64=$(cat /mnt/api-token/token | base64 -w 0 | head -c-1) + echo "proxy_set_header Authorization \"Bearer $token\";" >| \ + /mnt/nginx-generated-config/bearer.conf + echo "proxy_set_header Sec-WebSocket-Protocol \ + \"base64url.bearer.authorization.k8s.io.${token64}, \ + base64.binary.k8s.io\";" >| \ + /mnt/nginx-generated-config/websocket.conf + volumeMounts: + - name: nginx-generated-config + mountPath: /mnt/nginx-generated-config + - name: api-token + mountPath: /mnt/api-token + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1001 + resources: + limits: + cpu: 50m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + containers: + - image: openresty/openresty:1.25.3.1-0-jammy + name: nginx-120 + command: + - "/usr/local/openresty/bin/openresty" + - "-g" + - "daemon off;" + - -c + - /etc/nginx/nginx.conf + livenessProbe: + failureThreshold: 3 + httpGet: + path: /health + port: 9443 + scheme: HTTPS + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /health + port: 9443 + scheme: HTTPS + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 8080 + name: web + protocol: TCP + - containerPort: 9443 + name: web-tls + protocol: TCP + resources: + limits: + cpu: 300m + memory: 256Mi + requests: + cpu: 30m + memory: 128Mi + volumeMounts: + - mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + name: namespace-lister-proxy-auth + readOnly: true + - name: logs + mountPath: /var/log/nginx + - name: nginx-tmp + mountPath: /var/lib/nginx/tmp + - name: run + mountPath: /run + - name: serving-cert + mountPath: /mnt + - name: nginx-generated-config + mountPath: /mnt/nginx-generated-config + - name: openresty + mountPath: /var/run/openresty + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1001 + volumes: + - configMap: + defaultMode: 420 + name: namespace-lister-proxy-auth + items: + - key: nginx.conf + path: nginx.conf + name: namespace-lister-proxy-auth + - name: logs + emptyDir: {} + - name: nginx-tmp + emptyDir: {} + - name: run + emptyDir: {} + - name: serving-cert + secret: + secretName: serving-cert + - name: nginx-generated-config + emptyDir: {} + - name: api-token + secret: + secretName: namespace-lister-proxy-auth + - name: openresty + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: namespace-lister-proxy-auth + name: namespace-lister-proxy-auth +spec: + type: NodePort + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: web + port: 8888 + nodePort: 30010 + protocol: TCP + targetPort: web + - name: web-tls + port: 9443 + nodePort: 30011 + protocol: TCP + targetPort: web-tls + selector: + app: namespace-lister-proxy-auth diff --git a/acceptance/test/smart-proxy/config/proxy-auth/rbac.yaml b/acceptance/test/smart-proxy/config/proxy-auth/rbac.yaml new file mode 100644 index 0000000..bbfd23f --- /dev/null +++ b/acceptance/test/smart-proxy/config/proxy-auth/rbac.yaml @@ -0,0 +1,41 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: namespace-lister-proxy-auth + namespace: namespace-lister +--- +apiVersion: v1 +kind: Secret +metadata: + name: namespace-lister-proxy-auth + namespace: namespace-lister + annotations: + kubernetes.io/service-account.name: namespace-lister-proxy-auth +type: kubernetes.io/service-account-token +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: impersonate +rules: +- apiGroups: + - "" + resources: + - users + verbs: + - impersonate +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: acceptances-tests-user:proxy-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: impersonate +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: User + name: system:serviceaccount:namespace-lister:namespace-lister-proxy-auth diff --git a/acceptance/test/smart-proxy/features/list.feature b/acceptance/test/smart-proxy/features/list.feature new file mode 100644 index 0000000..572a0cb --- /dev/null +++ b/acceptance/test/smart-proxy/features/list.feature @@ -0,0 +1,5 @@ +Feature: List Namespaces + + Scenario: user list namespace + Given User has access to "10" namespaces + Then the User can retrieve only the namespaces they have access to diff --git a/acceptance/test/smart-proxy/features/read.feature b/acceptance/test/smart-proxy/features/read.feature new file mode 100644 index 0000000..825166d --- /dev/null +++ b/acceptance/test/smart-proxy/features/read.feature @@ -0,0 +1 @@ +Feature: List Namespaces diff --git a/acceptance/test/smart-proxy/hook_test.go b/acceptance/test/smart-proxy/hook_test.go new file mode 100644 index 0000000..568b640 --- /dev/null +++ b/acceptance/test/smart-proxy/hook_test.go @@ -0,0 +1,38 @@ +package acceptance + +import ( + "context" + + "github.com/cucumber/godog" + "sigs.k8s.io/controller-runtime/pkg/client" + + tcontext "github.com/konflux-ci/namespace-lister/acceptance/pkg/context" + arest "github.com/konflux-ci/namespace-lister/acceptance/pkg/rest" + "github.com/konflux-ci/namespace-lister/acceptance/pkg/suite" +) + +const defaultTestAddress string = "https://localhost:11443" + +func InjectHooks(ctx *godog.ScenarioContext) { + suite.InjectBaseHooks(ctx) + + ctx.Before(injectBuildUserClient) +} + +func injectBuildUserClient(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + return tcontext.WithBuildUserClientFunc(ctx, buildUserClientForAuthProxy), nil +} + +func buildUserClientForAuthProxy(ctx context.Context) (client.Client, error) { + // build impersonating client + cfg, err := arest.NewDefaultClientConfig() + if err != nil { + return nil, err + } + + user := tcontext.User(ctx) + cfg.Impersonate.UserName = user.Name + + cfg.Host = suite.EnvKonfluxAddressOrDefault(defaultTestAddress) + return arest.BuildClient(cfg) +} diff --git a/acceptance/test/smart-proxy/kind-config.yaml b/acceptance/test/smart-proxy/kind-config.yaml new file mode 100644 index 0000000..d183f3a --- /dev/null +++ b/acceptance/test/smart-proxy/kind-config.yaml @@ -0,0 +1,16 @@ +--- +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + system-reserved: memory=4Gi + extraPortMappings: + - containerPort: 30011 + hostPort: 11443 + protocol: TCP diff --git a/acceptance/test/smart-proxy/step_test.go b/acceptance/test/smart-proxy/step_test.go new file mode 100644 index 0000000..9be69e3 --- /dev/null +++ b/acceptance/test/smart-proxy/step_test.go @@ -0,0 +1,11 @@ +package acceptance + +import ( + "github.com/cucumber/godog" + + "github.com/konflux-ci/namespace-lister/acceptance/pkg/suite" +) + +func InjectSteps(ctx *godog.ScenarioContext) { + suite.InjectSteps(ctx) +} diff --git a/acceptance/test/smart-proxy/suite_test.go b/acceptance/test/smart-proxy/suite_test.go new file mode 100644 index 0000000..e8b80a0 --- /dev/null +++ b/acceptance/test/smart-proxy/suite_test.go @@ -0,0 +1,63 @@ +package acceptance + +import ( + "embed" + _ "embed" + "log" + "os" + "testing" + + "github.com/cucumber/godog" + "github.com/cucumber/godog/colors" + "github.com/spf13/pflag" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +//go:embed features/* +var features embed.FS + +var opts = godog.Options{ + Format: "pretty", + Paths: []string{"features"}, + FS: features, + Output: colors.Colored(os.Stdout), + Concurrency: 1, +} + +func init() { + logOpts := zap.Options{ + Development: true, + } + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&logOpts))) + + godog.BindCommandLineFlags("godog.", &opts) +} + +func TestMain(m *testing.M) { + // parse CLI arguments + pflag.Parse() + opts.Paths = pflag.Args() + + // run tests + sc := godog.TestSuite{ + ScenarioInitializer: InitializeScenario, + Options: &opts, + }.Run() + switch sc { + // 0 - success + case 0: + return + + // 1 - failed + // 2 - command line usage error + // 128 - or higher, os signal related error exit codes + default: + log.Fatalf("non-zero status returned (%d), failed to run feature tests", sc) + } +} + +func InitializeScenario(ctx *godog.ScenarioContext) { + InjectSteps(ctx) + InjectHooks(ctx) +} diff --git a/authenticator.go b/authenticator.go new file mode 100644 index 0000000..4109bef --- /dev/null +++ b/authenticator.go @@ -0,0 +1,93 @@ +package main + +import ( + "errors" + "net/http" + "os" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/apis/apiserver" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/authenticatorfactory" + "k8s.io/apiserver/pkg/authentication/user" + authenticationv1 "k8s.io/client-go/kubernetes/typed/authentication/v1" + "k8s.io/client-go/rest" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +type Authenticator struct { + usernameHeader string + next authenticator.Request +} + +func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + if a.usernameHeader != "" { + if username := req.Header.Get(a.usernameHeader); username != "" { + // TODO: parse User, Group, ServiceAccount. + // Look into Kubernetes libs before implementing it + return &authenticator.Response{ + User: &user.DefaultInfo{ + Name: username, + }, + }, true, nil + } + } + + return a.next.AuthenticateRequest(req) +} + +type AuthenticatorOptions struct { + Client rest.Interface + Config *rest.Config + Header string +} + +func NewAuthenticator(opts AuthenticatorOptions) (authenticator.Request, error) { + ar, _, err := newTokenReviewAuthenticatorWithOpts(&opts) + if err != nil { + return nil, err + } + + return &Authenticator{ + usernameHeader: opts.Header, + next: ar, + }, nil +} + +func NewTokenReviewAuthenticatorWithClient(c rest.Interface) (authenticator.Request, *spec.SecurityDefinitions, error) { + tokenAccessReviewClient := authenticationv1.New(c) + return newTokenReviewAuthenticator(tokenAccessReviewClient) +} + +func NewTokenReviewAuthenticatorWithConfig(cfg *rest.Config) (authenticator.Request, *spec.SecurityDefinitions, error) { + cfg = rest.CopyConfig(cfg) + tokenAccessReviewClient := authenticationv1.NewForConfigOrDie(cfg) + return newTokenReviewAuthenticator(tokenAccessReviewClient) +} + +func newTokenReviewAuthenticator(authenticationClient *authenticationv1.AuthenticationV1Client) (authenticator.Request, *spec.SecurityDefinitions, error) { + authCfg := authenticatorfactory.DelegatingAuthenticatorConfig{ + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: false}, + TokenAccessReviewClient: authenticationClient, + TokenAccessReviewTimeout: 1 * time.Minute, + WebhookRetryBackoff: &wait.Backoff{Duration: 2 * time.Second, Cap: 2 * time.Minute, Steps: 100, Factor: 2, Jitter: 2}, + CacheTTL: 5 * time.Minute, + } + return authCfg.New() +} + +func newTokenReviewAuthenticatorWithOpts(opts *AuthenticatorOptions) (authenticator.Request, *spec.SecurityDefinitions, error) { + switch { + case opts.Client != nil: + return NewTokenReviewAuthenticatorWithClient(opts.Client) + case opts.Config != nil: + return NewTokenReviewAuthenticatorWithConfig(opts.Config) + default: + return nil, nil, errors.New("one among client and config is required to build the TokenRevierAuthenticator") + } +} + +func GetUsernameHeaderFromEnv() string { + return os.Getenv(EnvUsernameHeader) +} diff --git a/authenticator_test.go b/authenticator_test.go new file mode 100644 index 0000000..0b41c35 --- /dev/null +++ b/authenticator_test.go @@ -0,0 +1,107 @@ +package main_test + +import ( + "context" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + "k8s.io/apiserver/pkg/authentication/authenticator" + + namespacelister "github.com/konflux-ci/namespace-lister" + "github.com/konflux-ci/namespace-lister/mocks" +) + +var _ = Describe("Authenticator", func() { + + var ( + ctrl *gomock.Controller + auth authenticator.Request + c *mocks.MockFakeInterface + ) + + userHeaderKey := "X-User-Header" + userHeaderValue := "my-user" + + BeforeEach(func(ctx context.Context) { + ctrl = gomock.NewController(GinkgoT()) + }) + + When("Header authentication is enabled", func() { + BeforeEach(func() { + // given + c = mocks.NewMockFakeInterface(ctrl) + a, err := namespacelister.NewAuthenticator(namespacelister.AuthenticatorOptions{ + Client: c, + Header: userHeaderKey, + }) + Expect(err).NotTo(HaveOccurred()) + + auth = a + }) + + It("returns user info from header", func(ctx context.Context) { + // given + r, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil) + Expect(err).NotTo(HaveOccurred()) + r.Header.Add(userHeaderKey, userHeaderValue) + + // when + rs, ok, err := auth.AuthenticateRequest(r) + + // then + Expect(err).NotTo(HaveOccurred()) + Expect(ok).To(BeTrue()) + Expect(rs).NotTo(BeNil()) + Expect(rs.User).NotTo(BeNil()) + Expect(rs.User.GetName()).To(BeEquivalentTo(userHeaderValue)) + }) + }) + + When("Header authentication is disabled", func() { + BeforeEach(func() { + // given + c = mocks.NewMockFakeInterface(ctrl) + a, err := namespacelister.NewAuthenticator(namespacelister.AuthenticatorOptions{ + Client: c, + }) + Expect(err).NotTo(HaveOccurred()) + + auth = a + }) + + It("ignores user info from header", func(ctx context.Context) { + // given + r, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil) + Expect(err).NotTo(HaveOccurred()) + r.Header.Add(userHeaderKey, userHeaderValue) + + // when + rs, ok, err := auth.AuthenticateRequest(r) + + // then + Expect(err).NotTo(HaveOccurred()) + Expect(ok).To(BeFalse()) + Expect(rs).To(BeNil()) + }) + + It("tries to validate the bearer token", func(ctx context.Context) { + // we expect the TokenReview API to be used to validate the token + c.EXPECT().Post().Times(1) + + // given + r, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil) + Expect(err).NotTo(HaveOccurred()) + r.Header.Add("Authorization", "Bearer invalid") + + // when + rs, ok, err := auth.AuthenticateRequest(r) + + // then + Expect(err).To(HaveOccurred()) + Expect(ok).To(BeFalse()) + Expect(rs).To(BeNil()) + }) + }) +}) diff --git a/cache.go b/cache.go index f09114a..66918fe 100644 --- a/cache.go +++ b/cache.go @@ -8,14 +8,12 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" ) -func BuildAndStartCache(ctx context.Context) (cache.Cache, error) { - cfg := ctrl.GetConfigOrDie() - +func BuildAndStartCache(ctx context.Context, cfg *rest.Config) (cache.Cache, error) { s := runtime.NewScheme() if err := corev1.AddToScheme(s); err != nil { return nil, err diff --git a/config/deployment.yaml b/config/deployment.yaml index d383275..92ef95e 100644 --- a/config/deployment.yaml +++ b/config/deployment.yaml @@ -25,8 +25,8 @@ spec: env: - name: LOG_LEVEL value: "0" - - name: HEADER_USERNAME - value: "Impersonate-User" + # - name: AUTH_USERNAME_HEADER + # value: "X-User" resources: limits: cpu: 500m @@ -43,14 +43,4 @@ spec: capabilities: drop: - "ALL" - volumes: - - name: "traefik-plugin-storage" - emptyDir: - sizeLimit: 20Mi - - name: "traefik-static-config" - configMap: - name: "traefik-sidecar-static-config" - - name: "traefik-dynamic-config" - configMap: - name: "traefik-sidecar-dynamic-config" terminationGracePeriodSeconds: 60 diff --git a/config/patches/with-header-auth.yaml b/config/patches/with-header-auth.yaml new file mode 100644 index 0000000..7d74e05 --- /dev/null +++ b/config/patches/with-header-auth.yaml @@ -0,0 +1,5 @@ +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: AUTH_USERNAME_HEADER + value: Impersonate-User diff --git a/config/rbac.yaml b/config/rbac.yaml index 9c86c92..b0d63ff 100644 --- a/config/rbac.yaml +++ b/config/rbac.yaml @@ -19,6 +19,20 @@ roleRef: name: namespace-lister-authorizer --- apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: namespace-lister-auth-delegator +subjects: +- apiGroup: "" + kind: ServiceAccount + name: namespace-lister + namespace: namespace-lister +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: namespace-lister-authorizer diff --git a/const.go b/const.go index e1a42d5..61b41e0 100644 --- a/const.go +++ b/const.go @@ -2,7 +2,7 @@ package main const ( EnvLogLevel string = "LOG_LEVEL" - EnvHeaderUsername string = "HEADER_USERNAME" + EnvUsernameHeader string = "AUTH_USERNAME_HEADER" EnvAddress string = "ADDRESS" DefaultAddr string = ":8080" diff --git a/context.go b/context.go index a3d9461..84af1a5 100644 --- a/context.go +++ b/context.go @@ -3,5 +3,6 @@ package main type ContextKey string const ( - ContextKeyLogger ContextKey = "logger" + ContextKeyLogger ContextKey = "logger" + ContextKeyUserDetails ContextKey = "user-details" ) diff --git a/env.go b/env.go index 65996fa..81f706d 100644 --- a/env.go +++ b/env.go @@ -5,10 +5,6 @@ import ( "os" ) -func getHeaderUsername() string { - return cmp.Or(os.Getenv(EnvHeaderUsername), DefaultHeaderUsername) -} - func getAddress() string { return cmp.Or(os.Getenv(EnvAddress), DefaultAddr) } diff --git a/go.mod b/go.mod index dadbec5..a33ba4b 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,12 @@ require ( github.com/go-logr/logr v1.4.2 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 + go.uber.org/mock v0.5.0 k8s.io/api v0.31.2 k8s.io/apimachinery v0.31.2 k8s.io/apiserver v0.31.2 + k8s.io/client-go v0.31.2 + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 k8s.io/kubernetes v1.31.2 sigs.k8s.io/controller-runtime v0.19.1 ) @@ -16,12 +19,15 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect @@ -34,6 +40,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -48,27 +55,38 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.22.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect - k8s.io/client-go v0.31.2 // indirect k8s.io/component-base v0.31.2 // indirect k8s.io/component-helpers v0.31.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index b070d2f..2224a78 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -15,12 +17,17 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -50,6 +57,8 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2 github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -108,8 +117,26 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= @@ -132,6 +159,8 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -149,14 +178,20 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -195,6 +230,8 @@ k8s.io/kubernetes v1.31.2 h1:VNSu4O7Xn5FFRsh9ePXyEPg6ucR21fOftarSdi053Gs= k8s.io/kubernetes v1.31.2/go.mod h1:9xmT2buyTYj8TRKwRae7FcuY8k5+xlxv7VivvO0KKfs= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/hack/tools/mockgen/go.mod b/hack/tools/mockgen/go.mod new file mode 100644 index 0000000..eeca9bd --- /dev/null +++ b/hack/tools/mockgen/go.mod @@ -0,0 +1,11 @@ +module github.com/konflux-ci/namespace-lister/hack/tools/mockgen + +go 1.22.0 + +require go.uber.org/mock v0.5.0 + +require ( + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/tools v0.22.0 // indirect +) diff --git a/hack/tools/mockgen/go.sum b/hack/tools/mockgen/go.sum new file mode 100644 index 0000000..06249b3 --- /dev/null +++ b/hack/tools/mockgen/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/tools/mockgen/tools.go b/hack/tools/mockgen/tools.go new file mode 100644 index 0000000..7602308 --- /dev/null +++ b/hack/tools/mockgen/tools.go @@ -0,0 +1,9 @@ +//go:build tools +// +build tools + +// This package imports things required by build scripts, to force `go mod` to see them as dependencies +package tools + +import ( + _ "go.uber.org/mock/mockgen" +) diff --git a/http_handler_list.go b/http_handler_list.go index d9c488e..5617b8c 100644 --- a/http_handler_list.go +++ b/http_handler_list.go @@ -7,19 +7,18 @@ import ( "net/http" kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apiserver/pkg/authentication/authenticator" ) var _ http.Handler = &ListNamespacesHandler{} type ListNamespacesHandler struct { - lister NamespaceLister - userHeader string + lister NamespaceLister } -func NewListNamespacesHandler(lister NamespaceLister, userHeader string) http.Handler { +func NewListNamespacesHandler(lister NamespaceLister) http.Handler { return &ListNamespacesHandler{ - lister: lister, - userHeader: userHeader, + lister: lister, } } @@ -28,8 +27,10 @@ func (h *ListNamespacesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request l := getLoggerFromContext(ctx) l.Info("received list request") + ud := r.Context().Value(ContextKeyUserDetails).(*authenticator.Response) + // retrieve projects as the user - nn, err := h.lister.ListNamespaces(r.Context(), r.Header.Get(h.userHeader)) + nn, err := h.lister.ListNamespaces(r.Context(), ud.User.GetName()) if err != nil { serr := &kerrors.StatusError{} if errors.As(err, &serr) { diff --git a/http_handler_list_test.go b/http_handler_list_test.go index 0754c04..e3a0c02 100644 --- a/http_handler_list_test.go +++ b/http_handler_list_test.go @@ -8,13 +8,16 @@ import ( "net/http" "net/http/httptest" - namespacelister "github.com/konflux-ci/namespace-lister" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + namespacelister "github.com/konflux-ci/namespace-lister" + corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" ) type NamespaceListerMock func(ctx context.Context, username string) (*corev1.NamespaceList, error) @@ -24,7 +27,19 @@ func (m NamespaceListerMock) ListNamespaces(ctx context.Context, username string } var _ = Describe("HttpHandlerList", func() { - const userHeader = "X-Email" + var request *http.Request + + BeforeEach(func(tctx context.Context) { + var err error + ctx := context.WithValue(tctx, namespacelister.ContextKeyUserDetails, + &authenticator.Response{ + User: &user.DefaultInfo{ + Name: "myuser", + }, + }) + request, err = http.NewRequestWithContext(ctx, http.MethodGet, "/", nil) + Expect(err).NotTo(HaveOccurred()) + }) DescribeTable("retrieves list of namespaces", func(expected corev1.NamespaceList) { // given @@ -35,22 +50,18 @@ var _ = Describe("HttpHandlerList", func() { lister := NamespaceListerMock(func(ctx context.Context, username string) (*corev1.NamespaceList, error) { return &expected, nil }) - handler := namespacelister.NewListNamespacesHandler(lister, userHeader) + handler := namespacelister.NewListNamespacesHandler(lister) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.Header.Add(userHeader, "myuser") // when - handler.ServeHTTP(w, r) + handler.ServeHTTP(w, request) // then Expect(w.Result()).NotTo(BeNil()) Expect(w.Result().StatusCode).To(Equal(http.StatusOK)) Expect(w.Result().Header.Get(namespacelister.HttpContentType)).To(Equal(namespacelister.HttpContentTypeApplication)) - wb, err := io.ReadAll(w.Result().Body) - Expect(err).NotTo(HaveOccurred()) - Expect(wb).To(Equal(eb)) + Expect(io.ReadAll(w.Result().Body)).To(Equal(eb)) }, Entry("empty list", corev1.NamespaceList{}), Entry("non empty list", corev1.NamespaceList{ @@ -69,22 +80,18 @@ var _ = Describe("HttpHandlerList", func() { lister := NamespaceListerMock(func(ctx context.Context, username string) (*corev1.NamespaceList, error) { return nil, expectedErr }) - handler := namespacelister.NewListNamespacesHandler(lister, userHeader) + handler := namespacelister.NewListNamespacesHandler(lister) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.Header.Add(userHeader, "myuser") // when - handler.ServeHTTP(w, r) + handler.ServeHTTP(w, request) // then Expect(w.Result()).NotTo(BeNil()) Expect(w.Result().StatusCode).To(Equal(expectedResponseStatus)) // Expect(w.Result().Header.Get(HttpContentType)).To(Equal(HttpContentTypeApplication)) - wb, err := io.ReadAll(w.Result().Body) - Expect(err).NotTo(HaveOccurred()) - Expect(wb).To(BeEquivalentTo(expectedErr.Error())) + Expect(io.ReadAll(w.Result().Body)).To(BeEquivalentTo(expectedErr.Error())) }, Entry("unhandled error", errors.New("unhandled error"), http.StatusInternalServerError), Entry("handled error", kerrors.NewTimeoutError("timed-out", 200), http.StatusGatewayTimeout), diff --git a/http_server.go b/http_server.go index a60e4b7..9e480e0 100644 --- a/http_server.go +++ b/http_server.go @@ -6,6 +6,8 @@ import ( "net/http" "os" "time" + + "k8s.io/apiserver/pkg/authentication/authenticator" ) const ( @@ -31,13 +33,41 @@ func addLogRequestMiddleware(next http.Handler) http.HandlerFunc { } } -func NewServer(l *slog.Logger, lister NamespaceLister, userHeader string) *NamespaceListerServer { +func addAuthnMiddleware(ar authenticator.Request, next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rs, ok, err := ar.AuthenticateRequest(r) + + switch { + case err != nil: // error contacting the APIServer for authenticating the request + w.WriteHeader(http.StatusUnauthorized) + l := getLoggerFromContext(r.Context()) + l.Error("error authenticating request", "error", err, "request-headers", r.Header) + return + + case !ok: // request could not be authenticated + w.WriteHeader(http.StatusUnauthorized) + return + + default: // request is authenticated + // Inject authentication details into request context + ctx := r.Context() + authCtx := context.WithValue(ctx, ContextKeyUserDetails, rs) + + // serve next request + next.ServeHTTP(w, r.WithContext(authCtx)) + } + } +} + +func NewServer(l *slog.Logger, ar authenticator.Request, lister NamespaceLister) *NamespaceListerServer { // configure the server h := http.NewServeMux() h.Handle(patternGetNamespaces, addInjectLoggerMiddleware(l, addLogRequestMiddleware( - NewListNamespacesHandler(lister, userHeader)))) + addAuthnMiddleware(ar, + NewListNamespacesHandler(lister))))) + return &NamespaceListerServer{ Server: &http.Server{ Addr: getAddress(), diff --git a/interfaces_test.go b/interfaces_test.go new file mode 100644 index 0000000..65200b7 --- /dev/null +++ b/interfaces_test.go @@ -0,0 +1,10 @@ +package main_test + +import ( + "k8s.io/client-go/rest" +) + +//go:generate mockgen -source=interfaces_test.go -destination=mocks/rest_interface.go -package=mocks +type FakeInterface interface { + rest.Interface +} diff --git a/log.go b/log.go index a3bbefd..232acdc 100644 --- a/log.go +++ b/log.go @@ -33,7 +33,7 @@ func setLoggerIntoContext(ctx context.Context, logger *slog.Logger) context.Cont } func getLoggerFromContext(ctx context.Context) *slog.Logger { - if l, ok := ctx.Value(ContextKeyLogger).(*slog.Logger); ok { + if l, ok := ctx.Value(ContextKeyLogger).(*slog.Logger); ok && l != nil { return l } diff --git a/main.go b/main.go index bc35a4b..28e3f21 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/go-logr/logr" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -23,6 +24,18 @@ func main() { func run(l *slog.Logger) error { log.SetLogger(logr.FromSlogHandler(l.Handler())) + // get config + cfg := ctrl.GetConfigOrDie() + + // build the request authenticator + ar, err := NewAuthenticator(AuthenticatorOptions{ + Config: cfg, + Header: GetUsernameHeaderFromEnv(), + }) + if err != nil { + return err + } + // setup context ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() @@ -31,7 +44,7 @@ func run(l *slog.Logger) error { // create cache l.Info("creating cache") - cache, err := BuildAndStartCache(ctx) + cache, err := BuildAndStartCache(ctx, cfg) if err != nil { return err } @@ -42,8 +55,7 @@ func run(l *slog.Logger) error { // build http server l.Info("building server") - userHeader := getHeaderUsername() - s := NewServer(l, nsl, userHeader) + s := NewServer(l, ar, nsl) // start the server return s.Start(ctx) diff --git a/mocks/rest_interface.go b/mocks/rest_interface.go new file mode 100644 index 0000000..8fb9583 --- /dev/null +++ b/mocks/rest_interface.go @@ -0,0 +1,156 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces_test.go +// +// Generated by this command: +// +// mockgen -source=interfaces_test.go -destination=mocks/rest_interface.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +// MockFakeInterface is a mock of FakeInterface interface. +type MockFakeInterface struct { + ctrl *gomock.Controller + recorder *MockFakeInterfaceMockRecorder + isgomock struct{} +} + +// MockFakeInterfaceMockRecorder is the mock recorder for MockFakeInterface. +type MockFakeInterfaceMockRecorder struct { + mock *MockFakeInterface +} + +// NewMockFakeInterface creates a new mock instance. +func NewMockFakeInterface(ctrl *gomock.Controller) *MockFakeInterface { + mock := &MockFakeInterface{ctrl: ctrl} + mock.recorder = &MockFakeInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFakeInterface) EXPECT() *MockFakeInterfaceMockRecorder { + return m.recorder +} + +// APIVersion mocks base method. +func (m *MockFakeInterface) APIVersion() schema.GroupVersion { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "APIVersion") + ret0, _ := ret[0].(schema.GroupVersion) + return ret0 +} + +// APIVersion indicates an expected call of APIVersion. +func (mr *MockFakeInterfaceMockRecorder) APIVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "APIVersion", reflect.TypeOf((*MockFakeInterface)(nil).APIVersion)) +} + +// Delete mocks base method. +func (m *MockFakeInterface) Delete() *rest.Request { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete") + ret0, _ := ret[0].(*rest.Request) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockFakeInterfaceMockRecorder) Delete() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockFakeInterface)(nil).Delete)) +} + +// Get mocks base method. +func (m *MockFakeInterface) Get() *rest.Request { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get") + ret0, _ := ret[0].(*rest.Request) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockFakeInterfaceMockRecorder) Get() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockFakeInterface)(nil).Get)) +} + +// GetRateLimiter mocks base method. +func (m *MockFakeInterface) GetRateLimiter() flowcontrol.RateLimiter { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRateLimiter") + ret0, _ := ret[0].(flowcontrol.RateLimiter) + return ret0 +} + +// GetRateLimiter indicates an expected call of GetRateLimiter. +func (mr *MockFakeInterfaceMockRecorder) GetRateLimiter() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRateLimiter", reflect.TypeOf((*MockFakeInterface)(nil).GetRateLimiter)) +} + +// Patch mocks base method. +func (m *MockFakeInterface) Patch(pt types.PatchType) *rest.Request { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Patch", pt) + ret0, _ := ret[0].(*rest.Request) + return ret0 +} + +// Patch indicates an expected call of Patch. +func (mr *MockFakeInterfaceMockRecorder) Patch(pt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockFakeInterface)(nil).Patch), pt) +} + +// Post mocks base method. +func (m *MockFakeInterface) Post() *rest.Request { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Post") + ret0, _ := ret[0].(*rest.Request) + return ret0 +} + +// Post indicates an expected call of Post. +func (mr *MockFakeInterfaceMockRecorder) Post() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockFakeInterface)(nil).Post)) +} + +// Put mocks base method. +func (m *MockFakeInterface) Put() *rest.Request { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put") + ret0, _ := ret[0].(*rest.Request) + return ret0 +} + +// Put indicates an expected call of Put. +func (mr *MockFakeInterfaceMockRecorder) Put() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockFakeInterface)(nil).Put)) +} + +// Verb mocks base method. +func (m *MockFakeInterface) Verb(verb string) *rest.Request { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verb", verb) + ret0, _ := ret[0].(*rest.Request) + return ret0 +} + +// Verb indicates an expected call of Verb. +func (mr *MockFakeInterfaceMockRecorder) Verb(verb any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verb", reflect.TypeOf((*MockFakeInterface)(nil).Verb), verb) +} diff --git a/namespacelister_test.go b/namespacelister_test.go index 61c9cc3..4a8ae63 100644 --- a/namespacelister_test.go +++ b/namespacelister_test.go @@ -17,9 +17,11 @@ import ( var _ = Describe("Namespacelister", func() { - var ( - ctx = context.TODO() - ) + var ctx context.Context + + BeforeEach(func(tctx context.Context) { + ctx = tctx //nolint:fatcontext + }) DescribeTable("when listing namespaces", func( nn corev1.NamespaceList,