From b042e7066f933a4f3b4d5aae498170fe3c226182 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 11:38:18 +0100 Subject: [PATCH 01/37] dexop: initialize --- go/dexop/.dockerignore | 3 + go/dexop/.gitignore | 27 ++ go/dexop/.golangci.yml | 40 +++ go/dexop/.tool-versions | 1 + go/dexop/Dockerfile | 33 ++ go/dexop/Makefile | 324 ++++++++++++++++++ go/dexop/PROJECT | 13 + go/dexop/README.md | 113 ++++++ go/dexop/cmd/main.go | 159 +++++++++ go/dexop/config/default/kustomization.yaml | 151 ++++++++ .../config/default/manager_metrics_patch.yaml | 4 + go/dexop/config/default/metrics_service.yaml | 17 + go/dexop/config/manager/kustomization.yaml | 2 + go/dexop/config/manager/manager.yaml | 95 +++++ go/dexop/config/manifests/kustomization.yaml | 28 ++ .../network-policy/allow-metrics-traffic.yaml | 26 ++ .../config/network-policy/kustomization.yaml | 2 + go/dexop/config/prometheus/kustomization.yaml | 2 + go/dexop/config/prometheus/monitor.yaml | 30 ++ go/dexop/config/rbac/kustomization.yaml | 20 ++ .../config/rbac/leader_election_role.yaml | 40 +++ .../rbac/leader_election_role_binding.yaml | 15 + go/dexop/config/rbac/metrics_auth_role.yaml | 17 + .../rbac/metrics_auth_role_binding.yaml | 12 + go/dexop/config/rbac/metrics_reader_role.yaml | 9 + go/dexop/config/rbac/role.yaml | 11 + go/dexop/config/rbac/role_binding.yaml | 15 + go/dexop/config/rbac/service_account.yaml | 8 + go/dexop/config/scorecard/bases/config.yaml | 7 + go/dexop/config/scorecard/kustomization.yaml | 18 + .../scorecard/patches/basic.config.yaml | 10 + .../config/scorecard/patches/olm.config.yaml | 50 +++ go/dexop/go.mod | 97 ++++++ go/dexop/go.sum | 251 ++++++++++++++ go/dexop/hack/boilerplate.go.txt | 15 + go/dexop/test/e2e/e2e_suite_test.go | 32 ++ go/dexop/test/e2e/e2e_test.go | 122 +++++++ go/dexop/test/utils/utils.go | 140 ++++++++ 38 files changed, 1959 insertions(+) create mode 100644 go/dexop/.dockerignore create mode 100644 go/dexop/.gitignore create mode 100644 go/dexop/.golangci.yml create mode 100644 go/dexop/.tool-versions create mode 100644 go/dexop/Dockerfile create mode 100644 go/dexop/Makefile create mode 100644 go/dexop/PROJECT create mode 100644 go/dexop/README.md create mode 100644 go/dexop/cmd/main.go create mode 100644 go/dexop/config/default/kustomization.yaml create mode 100644 go/dexop/config/default/manager_metrics_patch.yaml create mode 100644 go/dexop/config/default/metrics_service.yaml create mode 100644 go/dexop/config/manager/kustomization.yaml create mode 100644 go/dexop/config/manager/manager.yaml create mode 100644 go/dexop/config/manifests/kustomization.yaml create mode 100644 go/dexop/config/network-policy/allow-metrics-traffic.yaml create mode 100644 go/dexop/config/network-policy/kustomization.yaml create mode 100644 go/dexop/config/prometheus/kustomization.yaml create mode 100644 go/dexop/config/prometheus/monitor.yaml create mode 100644 go/dexop/config/rbac/kustomization.yaml create mode 100644 go/dexop/config/rbac/leader_election_role.yaml create mode 100644 go/dexop/config/rbac/leader_election_role_binding.yaml create mode 100644 go/dexop/config/rbac/metrics_auth_role.yaml create mode 100644 go/dexop/config/rbac/metrics_auth_role_binding.yaml create mode 100644 go/dexop/config/rbac/metrics_reader_role.yaml create mode 100644 go/dexop/config/rbac/role.yaml create mode 100644 go/dexop/config/rbac/role_binding.yaml create mode 100644 go/dexop/config/rbac/service_account.yaml create mode 100644 go/dexop/config/scorecard/bases/config.yaml create mode 100644 go/dexop/config/scorecard/kustomization.yaml create mode 100644 go/dexop/config/scorecard/patches/basic.config.yaml create mode 100644 go/dexop/config/scorecard/patches/olm.config.yaml create mode 100644 go/dexop/go.mod create mode 100644 go/dexop/go.sum create mode 100644 go/dexop/hack/boilerplate.go.txt create mode 100644 go/dexop/test/e2e/e2e_suite_test.go create mode 100644 go/dexop/test/e2e/e2e_test.go create mode 100644 go/dexop/test/utils/utils.go diff --git a/go/dexop/.dockerignore b/go/dexop/.dockerignore new file mode 100644 index 000000000..a3aab7af7 --- /dev/null +++ b/go/dexop/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/go/dexop/.gitignore b/go/dexop/.gitignore new file mode 100644 index 000000000..ada68ff08 --- /dev/null +++ b/go/dexop/.gitignore @@ -0,0 +1,27 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/go/dexop/.golangci.yml b/go/dexop/.golangci.yml new file mode 100644 index 000000000..ca69a11f6 --- /dev/null +++ b/go/dexop/.golangci.yml @@ -0,0 +1,40 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - exportloopref + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - staticcheck + - typecheck + - unconvert + - unparam + - unused diff --git a/go/dexop/.tool-versions b/go/dexop/.tool-versions new file mode 100644 index 000000000..1063f2595 --- /dev/null +++ b/go/dexop/.tool-versions @@ -0,0 +1 @@ +golang 1.23.8 diff --git a/go/dexop/Dockerfile b/go/dexop/Dockerfile new file mode 100644 index 000000000..a48973ee7 --- /dev/null +++ b/go/dexop/Dockerfile @@ -0,0 +1,33 @@ +# Build the manager binary +FROM golang:1.22 AS builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/controller/ internal/controller/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/go/dexop/Makefile b/go/dexop/Makefile new file mode 100644 index 000000000..4fb40e51e --- /dev/null +++ b/go/dexop/Makefile @@ -0,0 +1,324 @@ +# VERSION defines the project version for the bundle. +# Update this value when you upgrade the version of your project. +# To re-generate a bundle for another specific version without changing the standard setup, you can: +# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) +# - use environment variables to overwrite this value (e.g export VERSION=0.0.2) +VERSION ?= 0.0.1 + +# CHANNELS define the bundle channels used in the bundle. +# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") +# To re-generate a bundle for other specific channels without changing the standard setup, you can: +# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) +# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif + +# DEFAULT_CHANNEL defines the default channel used in the bundle. +# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") +# To re-generate a bundle for any other default channel without changing the default setup, you can: +# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) +# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. +# This variable is used to construct full image tags for bundle and catalog images. +# +# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both +# rax.io/dexop-bundle:$VERSION and rax.io/dexop-catalog:$VERSION. +IMAGE_TAG_BASE ?= rax.io/dexop + +# BUNDLE_IMG defines the image:tag used for the bundle. +# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) +BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) + +# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command +BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + +# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests +# You can enable this value if you would like to use SHA Based Digests +# To enable set flag to true +USE_IMAGE_DIGESTS ?= false +ifeq ($(USE_IMAGE_DIGESTS), true) + BUNDLE_GEN_FLAGS += --use-image-digests +endif + +# Set the Operator SDK version to use. By default, what is installed on the system is used. +# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. +OPERATOR_SDK_VERSION ?= unknown +# Image URL to use all building/pushing image targets +IMG ?= controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.31.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + +# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. +.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. +test-e2e: + go test ./test/e2e/ -v -ginkgo.v + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name dexop-builder + $(CONTAINER_TOOL) buildx use dexop-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm dexop-builder + rm Dockerfile.cross + +.PHONY: build-installer +build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. + mkdir -p dist + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > dist/install.yaml + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.4.3 +CONTROLLER_TOOLS_VERSION ?= v0.16.1 +ENVTEST_VERSION ?= release-0.19 +GOLANGCI_LINT_VERSION ?= v1.59.1 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. +$(ENVTEST): $(LOCALBIN) + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef + +.PHONY: operator-sdk +OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk +operator-sdk: ## Download operator-sdk locally if necessary. +ifeq (,$(wildcard $(OPERATOR_SDK))) +ifeq (, $(shell which operator-sdk 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPERATOR_SDK)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ + chmod +x $(OPERATOR_SDK) ;\ + } +else +OPERATOR_SDK = $(shell which operator-sdk) +endif +endif + +.PHONY: bundle +bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. + $(OPERATOR_SDK) generate kustomize manifests -q + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) + $(OPERATOR_SDK) bundle validate ./bundle + +.PHONY: bundle-build +bundle-build: ## Build the bundle image. + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + +.PHONY: bundle-push +bundle-push: ## Push the bundle image. + $(MAKE) docker-push IMG=$(BUNDLE_IMG) + +.PHONY: opm +OPM = $(LOCALBIN)/opm +opm: ## Download opm locally if necessary. +ifeq (,$(wildcard $(OPM))) +ifeq (,$(shell which opm 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPM)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ + chmod +x $(OPM) ;\ + } +else +OPM = $(shell which opm) +endif +endif + +# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). +# These images MUST exist in a registry and be pull-able. +BUNDLE_IMGS ?= $(BUNDLE_IMG) + +# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). +CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) + +# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. +ifneq ($(origin CATALOG_BASE_IMG), undefined) +FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) +endif + +# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. +# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: +# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator +.PHONY: catalog-build +catalog-build: opm ## Build a catalog image. + $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) + +# Push the catalog image. +.PHONY: catalog-push +catalog-push: ## Push a catalog image. + $(MAKE) docker-push IMG=$(CATALOG_IMG) diff --git a/go/dexop/PROJECT b/go/dexop/PROJECT new file mode 100644 index 000000000..9f23dfe9d --- /dev/null +++ b/go/dexop/PROJECT @@ -0,0 +1,13 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: rax.io +layout: +- go.kubebuilder.io/v4 +plugins: + manifests.sdk.operatorframework.io/v2: {} + scorecard.sdk.operatorframework.io/v2: {} +projectName: dexop +repo: github.com/rackerlabs/understack/go/dexop +version: "3" diff --git a/go/dexop/README.md b/go/dexop/README.md new file mode 100644 index 000000000..53fd41e19 --- /dev/null +++ b/go/dexop/README.md @@ -0,0 +1,113 @@ +# dexop +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started + +### Prerequisites +- go version v1.22.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. + +### To Deploy on the cluster +**Build and push your image to the location specified by `IMG`:** + +```sh +make docker-build docker-push IMG=/dexop:tag +``` + +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. + +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/dexop:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +>**NOTE**: Ensure that the samples has default values to test it out. + +### To Uninstall +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +## Project Distribution + +Following are the steps to build the installer and distribute this project to users. + +1. Build the installer for the image built and published in the registry: + +```sh +make build-installer IMG=/dexop:tag +``` + +NOTE: The makefile target mentioned above generates an 'install.yaml' +file in the dist directory. This file contains all the resources built +with Kustomize, which are necessary to install this project without +its dependencies. + +2. Using the installer + +Users can just run kubectl apply -f to install the project, i.e.: + +```sh +kubectl apply -f https://raw.githubusercontent.com//dexop//dist/install.yaml +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +**NOTE:** Run `make help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/go/dexop/cmd/main.go b/go/dexop/cmd/main.go new file mode 100644 index 000000000..07c864963 --- /dev/null +++ b/go/dexop/cmd/main.go @@ -0,0 +1,159 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "crypto/tls" + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + // +kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: tlsOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are + // not provided, self-signed certificates will be generated by default. This option is not recommended for + // production environments as self-signed certificates do not offer the same level of trust and security + // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing + // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName + // to provide certificates, ensuring the server communicates using trusted and secure certificates. + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "efbd5c69.rax.io", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + // +kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/go/dexop/config/default/kustomization.yaml b/go/dexop/config/default/kustomization.yaml new file mode 100644 index 000000000..93d0ed470 --- /dev/null +++ b/go/dexop/config/default/kustomization.yaml @@ -0,0 +1,151 @@ +# Adds namespace to all resources. +namespace: dexop-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: dexop- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +#- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus +# [METRICS] Expose the controller manager metrics service. +- metrics_service.yaml +# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. +# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. +# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will +# be able to communicate with the Webhook Server. +#- ../network-policy + +# Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager +patches: +# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. +# More info: https://book.kubebuilder.io/reference/metrics +- path: manager_metrics_patch.yaml + target: + kind: Deployment + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/go/dexop/config/default/manager_metrics_patch.yaml b/go/dexop/config/default/manager_metrics_patch.yaml new file mode 100644 index 000000000..2aaef6536 --- /dev/null +++ b/go/dexop/config/default/manager_metrics_patch.yaml @@ -0,0 +1,4 @@ +# This patch adds the args to allow exposing the metrics endpoint using HTTPS +- op: add + path: /spec/template/spec/containers/0/args/0 + value: --metrics-bind-address=:8443 diff --git a/go/dexop/config/default/metrics_service.yaml b/go/dexop/config/default/metrics_service.yaml new file mode 100644 index 000000000..cb56cea3e --- /dev/null +++ b/go/dexop/config/default/metrics_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager diff --git a/go/dexop/config/manager/kustomization.yaml b/go/dexop/config/manager/kustomization.yaml new file mode 100644 index 000000000..5c5f0b84c --- /dev/null +++ b/go/dexop/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/go/dexop/config/manager/manager.yaml b/go/dexop/config/manager/manager.yaml new file mode 100644 index 000000000..94bbe827c --- /dev/null +++ b/go/dexop/config/manager/manager.yaml @@ -0,0 +1,95 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/go/dexop/config/manifests/kustomization.yaml b/go/dexop/config/manifests/kustomization.yaml new file mode 100644 index 000000000..23f0ade52 --- /dev/null +++ b/go/dexop/config/manifests/kustomization.yaml @@ -0,0 +1,28 @@ +# These resources constitute the fully configured set of manifests +# used to generate the 'manifests/' directory in a bundle. +resources: +- bases/dexop.clusterserviceversion.yaml +- ../default +- ../samples +- ../scorecard + +# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. +# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. +# These patches remove the unnecessary "cert" volume and its manager container volumeMount. +#patches: +#- target: +# group: apps +# version: v1 +# kind: Deployment +# name: controller-manager +# namespace: system +# patch: |- +# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. +# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. +# - op: remove + +# path: /spec/template/spec/containers/0/volumeMounts/0 +# # Remove the "cert" volume, since OLM will create and mount a set of certs. +# # Update the indices in this path if adding or removing volumes in the manager's Deployment. +# - op: remove +# path: /spec/template/spec/volumes/0 diff --git a/go/dexop/config/network-policy/allow-metrics-traffic.yaml b/go/dexop/config/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 000000000..6f4d2527b --- /dev/null +++ b/go/dexop/config/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic +# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those +# namespaces are able to gathering data from the metrics endpoint. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: allow-metrics-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label metrics: enabled + - from: + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label + ports: + - port: 8443 + protocol: TCP diff --git a/go/dexop/config/network-policy/kustomization.yaml b/go/dexop/config/network-policy/kustomization.yaml new file mode 100644 index 000000000..ec0fb5e57 --- /dev/null +++ b/go/dexop/config/network-policy/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- allow-metrics-traffic.yaml diff --git a/go/dexop/config/prometheus/kustomization.yaml b/go/dexop/config/prometheus/kustomization.yaml new file mode 100644 index 000000000..ed137168a --- /dev/null +++ b/go/dexop/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/go/dexop/config/prometheus/monitor.yaml b/go/dexop/config/prometheus/monitor.yaml new file mode 100644 index 000000000..665285944 --- /dev/null +++ b/go/dexop/config/prometheus/monitor.yaml @@ -0,0 +1,30 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https # Ensure this is the name of the port that exposes HTTPS metrics + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables + # certificate verification. This poses a significant security risk by making the system vulnerable to + # man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between + # Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data, + # compromising the integrity and confidentiality of the information. + # Please use the following options for secure configurations: + # caFile: /etc/metrics-certs/ca.crt + # certFile: /etc/metrics-certs/tls.crt + # keyFile: /etc/metrics-certs/tls.key + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/go/dexop/config/rbac/kustomization.yaml b/go/dexop/config/rbac/kustomization.yaml new file mode 100644 index 000000000..5619aa009 --- /dev/null +++ b/go/dexop/config/rbac/kustomization.yaml @@ -0,0 +1,20 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# The following RBAC configurations are used to protect +# the metrics endpoint with authn/authz. These configurations +# ensure that only authorized users and service accounts +# can access the metrics endpoint. Comment the following +# permissions if you want to disable this protection. +# More info: https://book.kubebuilder.io/reference/metrics.html +- metrics_auth_role.yaml +- metrics_auth_role_binding.yaml +- metrics_reader_role.yaml diff --git a/go/dexop/config/rbac/leader_election_role.yaml b/go/dexop/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000..cca63679f --- /dev/null +++ b/go/dexop/config/rbac/leader_election_role.yaml @@ -0,0 +1,40 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/go/dexop/config/rbac/leader_election_role_binding.yaml b/go/dexop/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000..7f305c30f --- /dev/null +++ b/go/dexop/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/go/dexop/config/rbac/metrics_auth_role.yaml b/go/dexop/config/rbac/metrics_auth_role.yaml new file mode 100644 index 000000000..32d2e4ec6 --- /dev/null +++ b/go/dexop/config/rbac/metrics_auth_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-auth-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/go/dexop/config/rbac/metrics_auth_role_binding.yaml b/go/dexop/config/rbac/metrics_auth_role_binding.yaml new file mode 100644 index 000000000..e775d67ff --- /dev/null +++ b/go/dexop/config/rbac/metrics_auth_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: metrics-auth-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metrics-auth-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/go/dexop/config/rbac/metrics_reader_role.yaml b/go/dexop/config/rbac/metrics_reader_role.yaml new file mode 100644 index 000000000..51a75db47 --- /dev/null +++ b/go/dexop/config/rbac/metrics_reader_role.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/go/dexop/config/rbac/role.yaml b/go/dexop/config/rbac/role.yaml new file mode 100644 index 000000000..78f6d8182 --- /dev/null +++ b/go/dexop/config/rbac/role.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: manager-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] diff --git a/go/dexop/config/rbac/role_binding.yaml b/go/dexop/config/rbac/role_binding.yaml new file mode 100644 index 000000000..6da9e3c21 --- /dev/null +++ b/go/dexop/config/rbac/role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/go/dexop/config/rbac/service_account.yaml b/go/dexop/config/rbac/service_account.yaml new file mode 100644 index 000000000..86b74a0cd --- /dev/null +++ b/go/dexop/config/rbac/service_account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/go/dexop/config/scorecard/bases/config.yaml b/go/dexop/config/scorecard/bases/config.yaml new file mode 100644 index 000000000..c77047841 --- /dev/null +++ b/go/dexop/config/scorecard/bases/config.yaml @@ -0,0 +1,7 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: [] diff --git a/go/dexop/config/scorecard/kustomization.yaml b/go/dexop/config/scorecard/kustomization.yaml new file mode 100644 index 000000000..54e8aa507 --- /dev/null +++ b/go/dexop/config/scorecard/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +- bases/config.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: +- path: patches/basic.config.yaml + target: + group: scorecard.operatorframework.io + kind: Configuration + name: config + version: v1alpha3 +- path: patches/olm.config.yaml + target: + group: scorecard.operatorframework.io + kind: Configuration + name: config + version: v1alpha3 +# +kubebuilder:scaffold:patches diff --git a/go/dexop/config/scorecard/patches/basic.config.yaml b/go/dexop/config/scorecard/patches/basic.config.yaml new file mode 100644 index 000000000..8aa415864 --- /dev/null +++ b/go/dexop/config/scorecard/patches/basic.config.yaml @@ -0,0 +1,10 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:unknown + labels: + suite: basic + test: basic-check-spec-test diff --git a/go/dexop/config/scorecard/patches/olm.config.yaml b/go/dexop/config/scorecard/patches/olm.config.yaml new file mode 100644 index 000000000..47153a8b7 --- /dev/null +++ b/go/dexop/config/scorecard/patches/olm.config.yaml @@ -0,0 +1,50 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:unknown + labels: + suite: olm + test: olm-bundle-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:unknown + labels: + suite: olm + test: olm-crds-have-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:unknown + labels: + suite: olm + test: olm-crds-have-resources-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:unknown + labels: + suite: olm + test: olm-spec-descriptors-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:unknown + labels: + suite: olm + test: olm-status-descriptors-test diff --git a/go/dexop/go.mod b/go/dexop/go.mod new file mode 100644 index 000000000..9b7c9912a --- /dev/null +++ b/go/dexop/go.mod @@ -0,0 +1,97 @@ +module github.com/rackerlabs/understack/go/dexop + +go 1.22.0 + +require ( + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 + k8s.io/apimachinery v0.31.0 + k8s.io/client-go v0.31.0 + sigs.k8s.io/controller-runtime v0.19.0 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + 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-logr/zapr v1.3.0 // 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 + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/cel-go v0.20.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-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.6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/sethvargo/go-password v0.3.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // 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 + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.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/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 + 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/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/apiserver v0.31.0 // indirect + k8s.io/component-base v0.31.0 // 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-bc3834ca7and // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go/dexop/go.sum b/go/dexop/go.sum new file mode 100644 index 000000000..a8ec01da6 --- /dev/null +++ b/go/dexop/go.sum @@ -0,0 +1,251 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +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= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/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-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/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.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/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= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/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= +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-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.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= +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= +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.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.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/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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +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.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +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.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY= +k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk= +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/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= +k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= +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/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.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/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= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/go/dexop/hack/boilerplate.go.txt b/go/dexop/hack/boilerplate.go.txt new file mode 100644 index 000000000..49e0eee50 --- /dev/null +++ b/go/dexop/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/go/dexop/test/e2e/e2e_suite_test.go b/go/dexop/test/e2e/e2e_suite_test.go new file mode 100644 index 000000000..9d9e112d2 --- /dev/null +++ b/go/dexop/test/e2e/e2e_suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + _, _ = fmt.Fprintf(GinkgoWriter, "Starting dexop suite\n") + RunSpecs(t, "e2e suite") +} diff --git a/go/dexop/test/e2e/e2e_test.go b/go/dexop/test/e2e/e2e_test.go new file mode 100644 index 000000000..18423fec8 --- /dev/null +++ b/go/dexop/test/e2e/e2e_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "os/exec" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/rackerlabs/understack/go/dexop/test/utils" +) + +const namespace = "dexop-system" + +var _ = Describe("controller", Ordered, func() { + BeforeAll(func() { + By("installing prometheus operator") + Expect(utils.InstallPrometheusOperator()).To(Succeed()) + + By("installing the cert-manager") + Expect(utils.InstallCertManager()).To(Succeed()) + + By("creating manager namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + AfterAll(func() { + By("uninstalling the Prometheus manager bundle") + utils.UninstallPrometheusOperator() + + By("uninstalling the cert-manager bundle") + utils.UninstallCertManager() + + By("removing manager namespace") + cmd := exec.Command("kubectl", "delete", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + Context("Operator", func() { + It("should run successfully", func() { + var controllerPodName string + var err error + + // projectimage stores the name of the image used in the example + var projectimage = "example.com/dexop:v0.0.1" + + By("building the manager(Operator) image") + cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("loading the the manager(Operator) image on Kind") + err = utils.LoadImageToKindClusterWithName(projectimage) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("installing CRDs") + cmd = exec.Command("make", "install") + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("deploying the controller-manager") + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the controller-manager pod is running as expected") + verifyControllerUp := func() error { + // Get pod name + + cmd = exec.Command("kubectl", "get", + "pods", "-l", "control-plane=controller-manager", + "-o", "go-template={{ range .items }}"+ + "{{ if not .metadata.deletionTimestamp }}"+ + "{{ .metadata.name }}"+ + "{{ \"\\n\" }}{{ end }}{{ end }}", + "-n", namespace, + ) + + podOutput, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + podNames := utils.GetNonEmptyLines(string(podOutput)) + if len(podNames) != 1 { + return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) + } + controllerPodName = podNames[0] + ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager")) + + // Validate pod status + cmd = exec.Command("kubectl", "get", + "pods", controllerPodName, "-o", "jsonpath={.status.phase}", + "-n", namespace, + ) + status, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if string(status) != "Running" { + return fmt.Errorf("controller pod in %s status", status) + } + return nil + } + EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) + + }) + }) +}) diff --git a/go/dexop/test/utils/utils.go b/go/dexop/test/utils/utils.go new file mode 100644 index 000000000..da8f13c56 --- /dev/null +++ b/go/dexop/test/utils/utils.go @@ -0,0 +1,140 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "os" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo/v2" //nolint:golint,revive +) + +const ( + prometheusOperatorVersion = "v0.72.0" + prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + + "releases/download/%s/bundle.yaml" + + certmanagerVersion = "v1.14.4" + certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" +) + +func warnError(err error) { + _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) +} + +// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. +func InstallPrometheusOperator() error { + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "create", "-f", url) + _, err := Run(cmd) + return err +} + +// Run executes the provided command within this context +func Run(cmd *exec.Cmd) ([]byte, error) { + dir, _ := GetProjectDir() + cmd.Dir = dir + + if err := os.Chdir(cmd.Dir); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) + } + + cmd.Env = append(os.Environ(), "GO111MODULE=on") + command := strings.Join(cmd.Args, " ") + _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + output, err := cmd.CombinedOutput() + if err != nil { + return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + } + + return output, nil +} + +// UninstallPrometheusOperator uninstalls the prometheus +func UninstallPrometheusOperator() { + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + +// UninstallCertManager uninstalls the cert manager +func UninstallCertManager() { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + +// InstallCertManager installs the cert manager bundle. +func InstallCertManager() error { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "apply", "-f", url) + if _, err := Run(cmd); err != nil { + return err + } + // Wait for cert-manager-webhook to be ready, which can take time if cert-manager + // was re-installed after uninstalling on a cluster. + cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", + "--for", "condition=Available", + "--namespace", "cert-manager", + "--timeout", "5m", + ) + + _, err := Run(cmd) + return err +} + +// LoadImageToKindClusterWithName loads a local docker image to the kind cluster +func LoadImageToKindClusterWithName(name string) error { + cluster := "kind" + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { + cluster = v + } + kindOptions := []string{"load", "docker-image", name, "--name", cluster} + cmd := exec.Command("kind", kindOptions...) + _, err := Run(cmd) + return err +} + +// GetNonEmptyLines converts given command output string into individual objects +// according to line breakers, and ignores the empty elements in it. +func GetNonEmptyLines(output string) []string { + var res []string + elements := strings.Split(output, "\n") + for _, element := range elements { + if element != "" { + res = append(res, element) + } + } + + return res +} + +// GetProjectDir will return the directory where the project is +func GetProjectDir() (string, error) { + wd, err := os.Getwd() + if err != nil { + return wd, err + } + wd = strings.Replace(wd, "/test/e2e", "", -1) + return wd, nil +} From 171b1fd6b2779a294c2f3bff4b58615bb296c76c Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 11:56:18 +0100 Subject: [PATCH 02/37] dexop: fix broken go.mod and upgrade tools --- .typos.toml | 1 + go/dexop/.tool-versions | 2 +- go/dexop/go.mod | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.typos.toml b/.typos.toml index c1c3291e8..029b7d36f 100644 --- a/.typos.toml +++ b/.typos.toml @@ -6,6 +6,7 @@ extend-exclude = [ "schema/argo-workflows.json", "python/understack-workflows/tests/json_samples/", "containers/*/patches", + "go.mod" ] [default] diff --git a/go/dexop/.tool-versions b/go/dexop/.tool-versions index 1063f2595..4f160b628 100644 --- a/go/dexop/.tool-versions +++ b/go/dexop/.tool-versions @@ -1 +1 @@ -golang 1.23.8 +golang 1.23.6 diff --git a/go/dexop/go.mod b/go/dexop/go.mod index 9b7c9912a..f77eda337 100644 --- a/go/dexop/go.mod +++ b/go/dexop/go.mod @@ -91,7 +91,7 @@ require ( 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-bc3834ca7and // 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 ) From 3f1b14179c31e8c795e7588fbe0a8a85f2d283c4 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 11:59:30 +0100 Subject: [PATCH 03/37] dexop: generate Client resource --- go/dexop/PROJECT | 10 ++ go/dexop/api/v1alpha1/client_types.go | 64 ++++++++++ go/dexop/api/v1alpha1/groupversion_info.go | 36 ++++++ .../api/v1alpha1/zz_generated.deepcopy.go | 114 ++++++++++++++++++ go/dexop/cmd/main.go | 11 ++ go/dexop/config/crd/kustomization.yaml | 22 ++++ go/dexop/config/crd/kustomizeconfig.yaml | 19 +++ go/dexop/config/default/kustomization.yaml | 2 +- go/dexop/config/rbac/client_editor_role.yaml | 27 +++++ go/dexop/config/rbac/client_viewer_role.yaml | 23 ++++ go/dexop/config/rbac/kustomization.yaml | 6 + .../config/samples/dex_v1alpha1_client.yaml | 9 ++ go/dexop/config/samples/kustomization.yaml | 4 + .../internal/controller/client_controller.go | 62 ++++++++++ .../controller/client_controller_test.go | 84 +++++++++++++ go/dexop/internal/controller/suite_test.go | 96 +++++++++++++++ 16 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 go/dexop/api/v1alpha1/client_types.go create mode 100644 go/dexop/api/v1alpha1/groupversion_info.go create mode 100644 go/dexop/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 go/dexop/config/crd/kustomization.yaml create mode 100644 go/dexop/config/crd/kustomizeconfig.yaml create mode 100644 go/dexop/config/rbac/client_editor_role.yaml create mode 100644 go/dexop/config/rbac/client_viewer_role.yaml create mode 100644 go/dexop/config/samples/dex_v1alpha1_client.yaml create mode 100644 go/dexop/config/samples/kustomization.yaml create mode 100644 go/dexop/internal/controller/client_controller.go create mode 100644 go/dexop/internal/controller/client_controller_test.go create mode 100644 go/dexop/internal/controller/suite_test.go diff --git a/go/dexop/PROJECT b/go/dexop/PROJECT index 9f23dfe9d..c51f74950 100644 --- a/go/dexop/PROJECT +++ b/go/dexop/PROJECT @@ -10,4 +10,14 @@ plugins: scorecard.sdk.operatorframework.io/v2: {} projectName: dexop repo: github.com/rackerlabs/understack/go/dexop +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: rax.io + group: dex + kind: Client + path: github.com/rackerlabs/understack/go/dexop/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/go/dexop/api/v1alpha1/client_types.go b/go/dexop/api/v1alpha1/client_types.go new file mode 100644 index 000000000..1de93c2c8 --- /dev/null +++ b/go/dexop/api/v1alpha1/client_types.go @@ -0,0 +1,64 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ClientSpec defines the desired state of Client +type ClientSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Client. Edit client_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// ClientStatus defines the observed state of Client +type ClientStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Client is the Schema for the clients API +type Client struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClientSpec `json:"spec,omitempty"` + Status ClientStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClientList contains a list of Client +type ClientList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Client `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Client{}, &ClientList{}) +} diff --git a/go/dexop/api/v1alpha1/groupversion_info.go b/go/dexop/api/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..552df574b --- /dev/null +++ b/go/dexop/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the dex v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=dex.rax.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "dex.rax.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/go/dexop/api/v1alpha1/zz_generated.deepcopy.go b/go/dexop/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..ad4545d1e --- /dev/null +++ b/go/dexop/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Client) DeepCopyInto(out *Client) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Client. +func (in *Client) DeepCopy() *Client { + if in == nil { + return nil + } + out := new(Client) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Client) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientList) DeepCopyInto(out *ClientList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Client, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientList. +func (in *ClientList) DeepCopy() *ClientList { + if in == nil { + return nil + } + out := new(ClientList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClientList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientSpec) DeepCopyInto(out *ClientSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientSpec. +func (in *ClientSpec) DeepCopy() *ClientSpec { + if in == nil { + return nil + } + out := new(ClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientStatus) DeepCopyInto(out *ClientStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientStatus. +func (in *ClientStatus) DeepCopy() *ClientStatus { + if in == nil { + return nil + } + out := new(ClientStatus) + in.DeepCopyInto(out) + return out +} diff --git a/go/dexop/cmd/main.go b/go/dexop/cmd/main.go index 07c864963..285ed021d 100644 --- a/go/dexop/cmd/main.go +++ b/go/dexop/cmd/main.go @@ -34,6 +34,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + "github.com/rackerlabs/understack/go/dexop/internal/controller" // +kubebuilder:scaffold:imports ) @@ -45,6 +48,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(dexv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -140,6 +144,13 @@ func main() { os.Exit(1) } + if err = (&controller.ClientReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Client") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/go/dexop/config/crd/kustomization.yaml b/go/dexop/config/crd/kustomization.yaml new file mode 100644 index 000000000..7f8f1a386 --- /dev/null +++ b/go/dexop/config/crd/kustomization.yaml @@ -0,0 +1,22 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/dex.rax.io_clients.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_clients.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +#configurations: +#- kustomizeconfig.yaml diff --git a/go/dexop/config/crd/kustomizeconfig.yaml b/go/dexop/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000..ec5c150a9 --- /dev/null +++ b/go/dexop/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/go/dexop/config/default/kustomization.yaml b/go/dexop/config/default/kustomization.yaml index 93d0ed470..85923f745 100644 --- a/go/dexop/config/default/kustomization.yaml +++ b/go/dexop/config/default/kustomization.yaml @@ -15,7 +15,7 @@ namePrefix: dexop- # someName: someValue resources: -#- ../crd +- ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in diff --git a/go/dexop/config/rbac/client_editor_role.yaml b/go/dexop/config/rbac/client_editor_role.yaml new file mode 100644 index 000000000..e36bfba2a --- /dev/null +++ b/go/dexop/config/rbac/client_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit clients. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: client-editor-role +rules: +- apiGroups: + - dex.rax.io + resources: + - clients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dex.rax.io + resources: + - clients/status + verbs: + - get diff --git a/go/dexop/config/rbac/client_viewer_role.yaml b/go/dexop/config/rbac/client_viewer_role.yaml new file mode 100644 index 000000000..bab2977ca --- /dev/null +++ b/go/dexop/config/rbac/client_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view clients. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: client-viewer-role +rules: +- apiGroups: + - dex.rax.io + resources: + - clients + verbs: + - get + - list + - watch +- apiGroups: + - dex.rax.io + resources: + - clients/status + verbs: + - get diff --git a/go/dexop/config/rbac/kustomization.yaml b/go/dexop/config/rbac/kustomization.yaml index 5619aa009..da18ac5f8 100644 --- a/go/dexop/config/rbac/kustomization.yaml +++ b/go/dexop/config/rbac/kustomization.yaml @@ -18,3 +18,9 @@ resources: - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- client_editor_role.yaml +- client_viewer_role.yaml diff --git a/go/dexop/config/samples/dex_v1alpha1_client.yaml b/go/dexop/config/samples/dex_v1alpha1_client.yaml new file mode 100644 index 000000000..6eb555a83 --- /dev/null +++ b/go/dexop/config/samples/dex_v1alpha1_client.yaml @@ -0,0 +1,9 @@ +apiVersion: dex.rax.io/v1alpha1 +kind: Client +metadata: + labels: + app.kubernetes.io/name: dexop + app.kubernetes.io/managed-by: kustomize + name: client-sample +spec: + # TODO(user): Add fields here diff --git a/go/dexop/config/samples/kustomization.yaml b/go/dexop/config/samples/kustomization.yaml new file mode 100644 index 000000000..5d30b91b5 --- /dev/null +++ b/go/dexop/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- dex_v1alpha1_client.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go new file mode 100644 index 000000000..83a9d7dce --- /dev/null +++ b/go/dexop/internal/controller/client_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" +) + +// ClientReconciler reconciles a Client object +type ClientReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=dex.rax.io,resources=clients,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=dex.rax.io,resources=clients/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dex.rax.io,resources=clients/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Client object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile +func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ClientReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&dexv1alpha1.Client{}). + Complete(r) +} diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go new file mode 100644 index 000000000..207c14b7b --- /dev/null +++ b/go/dexop/internal/controller/client_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" +) + +var _ = Describe("Client Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + client := &dexv1alpha1.Client{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Client") + err := k8sClient.Get(ctx, typeNamespacedName, client) + if err != nil && errors.IsNotFound(err) { + resource := &dexv1alpha1.Client{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &dexv1alpha1.Client{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Client") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/go/dexop/internal/controller/suite_test.go b/go/dexop/internal/controller/suite_test.go new file mode 100644 index 000000000..2fb146e55 --- /dev/null +++ b/go/dexop/internal/controller/suite_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2025 Rackspace Technology. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = dexv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) From b2f57e0cf3356fed23aa30842c979f9d299187a5 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 12:00:40 +0100 Subject: [PATCH 04/37] dexop: example manifest --- go/dexop/config/samples/dex_v1alpha1_client.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/go/dexop/config/samples/dex_v1alpha1_client.yaml b/go/dexop/config/samples/dex_v1alpha1_client.yaml index 6eb555a83..93c232ce7 100644 --- a/go/dexop/config/samples/dex_v1alpha1_client.yaml +++ b/go/dexop/config/samples/dex_v1alpha1_client.yaml @@ -2,8 +2,10 @@ apiVersion: dex.rax.io/v1alpha1 kind: Client metadata: labels: - app.kubernetes.io/name: dexop - app.kubernetes.io/managed-by: kustomize - name: client-sample + name: fred-client spec: - # TODO(user): Add fields here + name: fred + secretName: fred-password + redirectURIs: + - http://localhost:8080 + - https://some.service.example.com/ From 9e84ef64af200fbf1c46a845c865a23c9e8d648d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 12:39:35 +0100 Subject: [PATCH 05/37] dexop: install dex client APIs --- go/dexop/go.mod | 33 ++++++++++++++++++--------------- go/dexop/go.sum | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/go/dexop/go.mod b/go/dexop/go.mod index f77eda337..6ac880ac3 100644 --- a/go/dexop/go.mod +++ b/go/dexop/go.mod @@ -1,6 +1,8 @@ module github.com/rackerlabs/understack/go/dexop -go 1.22.0 +go 1.23 + +toolchain go1.23.6 require ( github.com/onsi/ginkgo/v2 v2.19.0 @@ -18,6 +20,7 @@ require ( 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/dexidp/dex/api/v2 v2.3.0 // 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 @@ -58,29 +61,29 @@ require ( github.com/stoewer/go-strcase v1.2.0 // 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 v1.32.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/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.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/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/net v0.33.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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 + google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // 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/go/dexop/go.sum b/go/dexop/go.sum index a8ec01da6..df7a7d729 100644 --- a/go/dexop/go.sum +++ b/go/dexop/go.sum @@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dexidp/dex/api/v2 v2.3.0 h1:gv79YqTBTGU6z/QE3RNFzlS6KrPRUudVUN8o858gpCc= +github.com/dexidp/dex/api/v2 v2.3.0/go.mod h1:y9As69T4WZOERCS/CfB9D8Dbb12tafU9ywv2ZZLf4EI= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -136,16 +138,20 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIX 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 v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= 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/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= 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/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= 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/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= 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= @@ -167,24 +173,33 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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/sync v0.10.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= 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/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.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/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= @@ -201,12 +216,20 @@ 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/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E= +google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= 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/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 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/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From d5138e70685d65d3d40ffc11f94449aecd72ed38 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 14:50:58 +0100 Subject: [PATCH 06/37] dexop: add Dex client --- go/dexop/api/v1alpha1/client_types.go | 12 +- go/dexop/dex/client.go | 108 ++++++++++++++++++ .../internal/controller/client_controller.go | 9 +- 3 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 go/dexop/dex/client.go diff --git a/go/dexop/api/v1alpha1/client_types.go b/go/dexop/api/v1alpha1/client_types.go index 1de93c2c8..256614d0b 100644 --- a/go/dexop/api/v1alpha1/client_types.go +++ b/go/dexop/api/v1alpha1/client_types.go @@ -20,16 +20,18 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // ClientSpec defines the desired state of Client type ClientSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - // Foo is an example field of Client. Edit client_types.go to remove/update - Foo string `json:"foo,omitempty"` + Name string `json:"name"` + SecretName string `json:"secretName,omitempty"` + SecretNamespace string `json:"secretNamespace,omitempty"` + RedirectURIs []string `json:"redirectURIs"` + LogoUrl string `json:"logoURL,omitempty"` + TrustedPeers []string `json:"trustedPeers,omitempty"` + Public bool `json:"public,omitempty"` } // ClientStatus defines the observed state of Client diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go new file mode 100644 index 000000000..cf756ca2f --- /dev/null +++ b/go/dexop/dex/client.go @@ -0,0 +1,108 @@ +package dex + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "os" + + dexapi "github.com/dexidp/dex/api/v2" + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type DexManager struct { + Client dexapi.DexClient +} + +func (d *DexManager) init() { + // start of dex iface + client, err := newDexClient("127.0.0.1:5557", "./grpc_ca.crt", "./grpc_client.key", "./grpc_client.crt") + if err != nil { + // ctrl.Log.Error(err, "failed creating dex client") + } + d.Client = client +} + +// Creates new Oauth2 client in Dex +func (d *DexManager) CreateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.CreateClientResp, error) { + request := &dexapi.CreateClientReq{ + Client: &dexapi.Client{ + Id: clientSpec.Spec.Name, + Secret: clientSpec.Spec.SecretName, + RedirectUris: clientSpec.Spec.RedirectURIs, + TrustedPeers: clientSpec.Spec.TrustedPeers, + Public: clientSpec.Spec.Public, + Name: clientSpec.Spec.Name, + LogoUrl: clientSpec.Spec.LogoUrl, + }, + } + + return d.Client.CreateClient(context.TODO(), request) +} + +// Deletes an Oauth2 client +func (d *DexManager) RemoveOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.DeleteClientResp, error) { + request := &dexapi.DeleteClientReq{ + Id: clientSpec.Spec.Name, + } + return d.Client.DeleteClient(context.TODO(), request) +} + +// Patches/updates the Oauth2 client + +func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.UpdateClientResp, error) { + request := &dexapi.GetClientReq{ + Id: clientSpec.Spec.Name, + } + + existing, err := d.Client.GetClient(context.TODO(), request) + if err != nil { + return nil, err + } + + if existing == nil { + return nil, fmt.Errorf("oauth2 client id: %s does not exist in Dex", clientSpec.Spec.Name) + } + + if existing.Client.Secret != clientSpec.Spec.SecretName || + existing.Client.Public != clientSpec.Spec.Public { + // dex does not support secret updates so it needs to be recreated + } + + updateRequest := &dexapi.UpdateClientReq{ + Id: clientSpec.Spec.Name, + RedirectUris: clientSpec.Spec.RedirectURIs, + } + return d.Client.UpdateClient(context.TODO(), updateRequest) +} + +func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexClient, error) { + cPool := x509.NewCertPool() + caCert, err := os.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("invalid CA crt file: %s", caPath) + } + if cPool.AppendCertsFromPEM(caCert) != true { + return nil, fmt.Errorf("failed to parse CA crt") + } + + clientCert, err := tls.LoadX509KeyPair(clientCrt, clientKey) + if err != nil { + return nil, fmt.Errorf("invalid client crt file: %s", clientCrt) + } + + clientTLSConfig := &tls.Config{ + RootCAs: cPool, + Certificates: []tls.Certificate{clientCert}, + } + creds := credentials.NewTLS(clientTLSConfig) + + conn, err := grpc.Dial(hostAndPort, grpc.WithTransportCredentials(creds)) + if err != nil { + return nil, fmt.Errorf("dial: %v", err) + } + return dexapi.NewDexClient(conn), nil +} diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 83a9d7dce..e520fec24 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + dexmgr "github.com/rackerlbals/understack/go/dexop/dex/client" ) // ClientReconciler reconciles a Client object @@ -49,8 +50,12 @@ type ClientReconciler struct { func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) - // TODO(user): your logic here - + clientSpec := &dexv1alpha1.Client{} + if err := r.Get(ctx, req.NamespacedName, clientSpec); err != nil { + return ctrl.Result{}, err + } + ctrl.Log.Info("reconciling Client", "Name", clientSpec.Spec.Name) + mgr := new(dexmgr.DexManager) return ctrl.Result{}, nil } From 2b6391c21581a15aa8b60cb836c99a8e664055b7 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 18:01:04 +0100 Subject: [PATCH 07/37] dexop: handle create and delete correctly The update should work too. --- .../api/v1alpha1/zz_generated.deepcopy.go | 12 ++- .../config/crd/bases/dex.rax.io_clients.yaml | 71 ++++++++++++++++++ go/dexop/config/rbac/role.yaml | 33 ++++++-- go/dexop/dex/client.go | 33 ++++---- go/dexop/go.mod | 7 +- go/dexop/go.sum | 37 +++------ .../internal/controller/client_controller.go | 75 ++++++++++++++++++- 7 files changed, 213 insertions(+), 55 deletions(-) create mode 100644 go/dexop/config/crd/bases/dex.rax.io_clients.yaml diff --git a/go/dexop/api/v1alpha1/zz_generated.deepcopy.go b/go/dexop/api/v1alpha1/zz_generated.deepcopy.go index ad4545d1e..6f1df615e 100644 --- a/go/dexop/api/v1alpha1/zz_generated.deepcopy.go +++ b/go/dexop/api/v1alpha1/zz_generated.deepcopy.go @@ -29,7 +29,7 @@ func (in *Client) DeepCopyInto(out *Client) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -86,6 +86,16 @@ func (in *ClientList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientSpec) DeepCopyInto(out *ClientSpec) { *out = *in + if in.RedirectURIs != nil { + in, out := &in.RedirectURIs, &out.RedirectURIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TrustedPeers != nil { + in, out := &in.TrustedPeers, &out.TrustedPeers + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientSpec. diff --git a/go/dexop/config/crd/bases/dex.rax.io_clients.yaml b/go/dexop/config/crd/bases/dex.rax.io_clients.yaml new file mode 100644 index 000000000..61ca9460e --- /dev/null +++ b/go/dexop/config/crd/bases/dex.rax.io_clients.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: clients.dex.rax.io +spec: + group: dex.rax.io + names: + kind: Client + listKind: ClientList + plural: clients + singular: client + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Client is the Schema for the clients API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClientSpec defines the desired state of Client + properties: + logoURL: + type: string + name: + type: string + public: + type: boolean + redirectURIs: + items: + type: string + type: array + secretName: + type: string + secretNamespace: + type: string + trustedPeers: + items: + type: string + type: array + required: + - name + - redirectURIs + type: object + status: + description: ClientStatus defines the observed state of Client + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/go/dexop/config/rbac/role.yaml b/go/dexop/config/rbac/role.yaml index 78f6d8182..42737d6cb 100644 --- a/go/dexop/config/rbac/role.yaml +++ b/go/dexop/config/rbac/role.yaml @@ -1,11 +1,32 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: dexop - app.kubernetes.io/managed-by: kustomize name: manager-role rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] +- apiGroups: + - dex.rax.io + resources: + - clients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dex.rax.io + resources: + - clients/finalizers + verbs: + - update +- apiGroups: + - dex.rax.io + resources: + - clients/status + verbs: + - get + - patch + - update diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index cf756ca2f..4e53a51b2 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -17,15 +17,6 @@ type DexManager struct { Client dexapi.DexClient } -func (d *DexManager) init() { - // start of dex iface - client, err := newDexClient("127.0.0.1:5557", "./grpc_ca.crt", "./grpc_client.key", "./grpc_client.crt") - if err != nil { - // ctrl.Log.Error(err, "failed creating dex client") - } - d.Client = client -} - // Creates new Oauth2 client in Dex func (d *DexManager) CreateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.CreateClientResp, error) { request := &dexapi.CreateClientReq{ @@ -44,21 +35,25 @@ func (d *DexManager) CreateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi } // Deletes an Oauth2 client -func (d *DexManager) RemoveOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.DeleteClientResp, error) { +func (d *DexManager) RemoveOauth2Client(clientId string) (*dexapi.DeleteClientResp, error) { request := &dexapi.DeleteClientReq{ - Id: clientSpec.Spec.Name, + Id: clientId, } return d.Client.DeleteClient(context.TODO(), request) } +func (d *DexManager) GetOauth2Client(clientId string) (*dexapi.GetClientResp, error) { + request := &dexapi.GetClientReq{ + Id: clientId, + } + return d.Client.GetClient(context.TODO(), request) +} + // Patches/updates the Oauth2 client func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.UpdateClientResp, error) { - request := &dexapi.GetClientReq{ - Id: clientSpec.Spec.Name, - } - existing, err := d.Client.GetClient(context.TODO(), request) + existing, err := d.GetOauth2Client(clientSpec.Spec.Name) if err != nil { return nil, err } @@ -106,3 +101,11 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC } return dexapi.NewDexClient(conn), nil } + +func NewDexManager(host, caCert, clientKey, clientCert string) (*DexManager, error) { + client, err := newDexClient(host, caCert, clientKey, clientCert) + if err != nil { + return nil, fmt.Errorf("failed creating dex client: %w", err) + } + return &DexManager{Client: client}, nil +} diff --git a/go/dexop/go.mod b/go/dexop/go.mod index 6ac880ac3..cbb7214ca 100644 --- a/go/dexop/go.mod +++ b/go/dexop/go.mod @@ -5,8 +5,13 @@ go 1.23 toolchain go1.23.6 require ( + github.com/dexidp/dex/api/v2 v2.3.0 + github.com/go-logr/logr v1.4.2 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 + github.com/sethvargo/go-password v0.3.1 + google.golang.org/grpc v1.70.0 + k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 sigs.k8s.io/controller-runtime v0.19.0 @@ -20,7 +25,6 @@ require ( 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/dexidp/dex/api/v2 v2.3.0 // 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 @@ -82,7 +86,6 @@ require ( gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect - google.golang.org/grpc v1.70.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go/dexop/go.sum b/go/dexop/go.sum index df7a7d729..c0ee9a785 100644 --- a/go/dexop/go.sum +++ b/go/dexop/go.sum @@ -136,21 +136,19 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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 v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= 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/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -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/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -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/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= @@ -171,33 +169,24 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -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/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.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= -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/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.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/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -214,20 +203,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E= google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= -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/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -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/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index e520fec24..21e71afbf 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -18,16 +18,22 @@ package controller import ( "context" + "strings" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/go-logr/logr" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" - dexmgr "github.com/rackerlbals/understack/go/dexop/dex/client" + dexmgr "github.com/rackerlabs/understack/go/dexop/dex" ) +const dexFinalizer = "dex.rax.io/finalizer" + // ClientReconciler reconciles a Client object type ClientReconciler struct { client.Client @@ -49,13 +55,68 @@ type ClientReconciler struct { // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) + reqLogger := ctrl.Log.WithValues("client", req.NamespacedName) + + mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "./grpc_ca.crt", "./grpc_client.key", "./grpc_client.crt") + if err != nil { + ctrl.Log.Error(err, "While getting the DexManager") + return ctrl.Result{}, err + } clientSpec := &dexv1alpha1.Client{} if err := r.Get(ctx, req.NamespacedName, clientSpec); err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + reqLogger.Info("reconciling Client") + + // delete if no longer needed + deleteRequested := clientSpec.GetDeletionTimestamp() != nil + if deleteRequested { + if controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { + if err := r.finalizeDeletion(reqLogger, clientSpec, mgr); err != nil { + return ctrl.Result{}, err + } + + // remove finalizer + controllerutil.RemoveFinalizer(clientSpec, dexFinalizer) + err := r.Update(ctx, clientSpec) + if err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // add finalizer + if !controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { + controllerutil.AddFinalizer(clientSpec, dexFinalizer) + err := r.Update(ctx, clientSpec) + if err != nil { + return ctrl.Result{}, err + } + } + + existing, err := mgr.GetOauth2Client(clientSpec.Spec.Name) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + ctrl.Log.Info("Client does not exist in Dex. Creating one.", "name", clientSpec.Spec.Name) + if _, err = mgr.CreateOauth2Client(clientSpec); err != nil { + reqLogger.Error(err, "Unable to create client in dex") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } return ctrl.Result{}, err } - ctrl.Log.Info("reconciling Client", "Name", clientSpec.Spec.Name) - mgr := new(dexmgr.DexManager) + + // update + if existing != nil { + mgr.UpdateOauth2Client(clientSpec) + } return ctrl.Result{}, nil } @@ -65,3 +126,11 @@ func (r *ClientReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&dexv1alpha1.Client{}). Complete(r) } + +func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha1.Client, mgr *dexmgr.DexManager) error { + reqLogger.Info("Client is being removed") + if _, err := mgr.RemoveOauth2Client(c.Spec.Name); err != nil { + return err + } + return nil +} From f166fe2cda7f91e46448f25d07695609ee4bd8a7 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 18:30:31 +0100 Subject: [PATCH 08/37] dexop: UpdateOauth2Client can set logo and trusted peers --- go/dexop/dex/client.go | 4 +++- go/dexop/internal/controller/client_controller.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index 4e53a51b2..2a274b42f 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -52,7 +52,6 @@ func (d *DexManager) GetOauth2Client(clientId string) (*dexapi.GetClientResp, er // Patches/updates the Oauth2 client func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.UpdateClientResp, error) { - existing, err := d.GetOauth2Client(clientSpec.Spec.Name) if err != nil { return nil, err @@ -70,6 +69,9 @@ func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi updateRequest := &dexapi.UpdateClientReq{ Id: clientSpec.Spec.Name, RedirectUris: clientSpec.Spec.RedirectURIs, + TrustedPeers: clientSpec.Spec.TrustedPeers, + LogoUrl: clientSpec.Spec.LogoUrl, + Name: clientSpec.Spec.Name, } return d.Client.UpdateClient(context.TODO(), updateRequest) } diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 21e71afbf..5c5364398 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -115,6 +115,7 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // update if existing != nil { + reqLogger.Info("making an UpdateOauth2Client call") mgr.UpdateOauth2Client(clientSpec) } return ctrl.Result{}, nil From b26af2684d424b3037da021958a06b8e3a768652 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 15 Apr 2025 19:17:35 +0100 Subject: [PATCH 09/37] dexop: add support for reading Secrets This only works on creation for now. The "update" case does not work because Dex API does not expose this. --- go/dexop/api/v1alpha1/client_types.go | 1 + .../config/samples/dex_v1alpha1_client.yaml | 2 +- go/dexop/dex/client.go | 4 +-- .../internal/controller/client_controller.go | 35 ++++++++++++++++--- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/go/dexop/api/v1alpha1/client_types.go b/go/dexop/api/v1alpha1/client_types.go index 256614d0b..3ea5bcc44 100644 --- a/go/dexop/api/v1alpha1/client_types.go +++ b/go/dexop/api/v1alpha1/client_types.go @@ -28,6 +28,7 @@ type ClientSpec struct { Name string `json:"name"` SecretName string `json:"secretName,omitempty"` SecretNamespace string `json:"secretNamespace,omitempty"` + SecretValue string `json:"-"` RedirectURIs []string `json:"redirectURIs"` LogoUrl string `json:"logoURL,omitempty"` TrustedPeers []string `json:"trustedPeers,omitempty"` diff --git a/go/dexop/config/samples/dex_v1alpha1_client.yaml b/go/dexop/config/samples/dex_v1alpha1_client.yaml index 93c232ce7..28006b600 100644 --- a/go/dexop/config/samples/dex_v1alpha1_client.yaml +++ b/go/dexop/config/samples/dex_v1alpha1_client.yaml @@ -5,7 +5,7 @@ metadata: name: fred-client spec: name: fred - secretName: fred-password + secretName: my-secret redirectURIs: - http://localhost:8080 - https://some.service.example.com/ diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index 2a274b42f..c55ab4cb1 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -22,7 +22,7 @@ func (d *DexManager) CreateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi request := &dexapi.CreateClientReq{ Client: &dexapi.Client{ Id: clientSpec.Spec.Name, - Secret: clientSpec.Spec.SecretName, + Secret: clientSpec.Spec.SecretValue, RedirectUris: clientSpec.Spec.RedirectURIs, TrustedPeers: clientSpec.Spec.TrustedPeers, Public: clientSpec.Spec.Public, @@ -61,7 +61,7 @@ func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi return nil, fmt.Errorf("oauth2 client id: %s does not exist in Dex", clientSpec.Spec.Name) } - if existing.Client.Secret != clientSpec.Spec.SecretName || + if existing.Client.Secret != clientSpec.Spec.SecretValue || existing.Client.Public != clientSpec.Spec.Public { // dex does not support secret updates so it needs to be recreated } diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 5c5364398..b337cda5f 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -18,18 +18,19 @@ package controller import ( "context" + "fmt" "strings" + "github.com/go-logr/logr" + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + dexmgr "github.com/rackerlabs/understack/go/dexop/dex" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/go-logr/logr" - dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" - dexmgr "github.com/rackerlabs/understack/go/dexop/dex" ) const dexFinalizer = "dex.rax.io/finalizer" @@ -100,6 +101,18 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } } + // if secretName specified, read the secret + if clientSpec.Spec.SecretName != "" { + if clientSpec.Spec.SecretNamespace == "" { + clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace + } + value, err := r.readSecret(ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + if err != nil { + reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) + } + clientSpec.Spec.SecretValue = value + } + existing, err := mgr.GetOauth2Client(clientSpec.Spec.Name) if err != nil { if strings.Contains(strings.ToLower(err.Error()), "not found") { @@ -135,3 +148,17 @@ func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha } return nil } + +func (r *ClientReconciler) readSecret(ctx context.Context, name, namespace string) (string, error) { + secret := &v1.Secret{} + + err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, secret) + if err != nil { + return "", err + } + + if value, ok := secret.Data["secret"]; ok { + return string(value), nil + } + return "", fmt.Errorf("secret key not found") +} From 92252d14e8bf70a220eff1be2801b709ca2e99b1 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 16 Apr 2025 10:25:46 +0100 Subject: [PATCH 10/37] dexop: add support for generating secrets ...with a fixed password for now --- go/dexop/api/v1alpha1/client_types.go | 1 + .../config/crd/bases/dex.rax.io_clients.yaml | 2 + .../dex_v1alpha1_client_generate_secret.yaml | 12 ++++++ .../internal/controller/client_controller.go | 42 ++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 go/dexop/config/samples/dex_v1alpha1_client_generate_secret.yaml diff --git a/go/dexop/api/v1alpha1/client_types.go b/go/dexop/api/v1alpha1/client_types.go index 3ea5bcc44..a1993047a 100644 --- a/go/dexop/api/v1alpha1/client_types.go +++ b/go/dexop/api/v1alpha1/client_types.go @@ -29,6 +29,7 @@ type ClientSpec struct { SecretName string `json:"secretName,omitempty"` SecretNamespace string `json:"secretNamespace,omitempty"` SecretValue string `json:"-"` + GenerateSecret bool `json:"generateSecret,omitempty"` RedirectURIs []string `json:"redirectURIs"` LogoUrl string `json:"logoURL,omitempty"` TrustedPeers []string `json:"trustedPeers,omitempty"` diff --git a/go/dexop/config/crd/bases/dex.rax.io_clients.yaml b/go/dexop/config/crd/bases/dex.rax.io_clients.yaml index 61ca9460e..304056802 100644 --- a/go/dexop/config/crd/bases/dex.rax.io_clients.yaml +++ b/go/dexop/config/crd/bases/dex.rax.io_clients.yaml @@ -39,6 +39,8 @@ spec: spec: description: ClientSpec defines the desired state of Client properties: + generateSecret: + type: boolean logoURL: type: string name: diff --git a/go/dexop/config/samples/dex_v1alpha1_client_generate_secret.yaml b/go/dexop/config/samples/dex_v1alpha1_client_generate_secret.yaml new file mode 100644 index 000000000..7261fc76b --- /dev/null +++ b/go/dexop/config/samples/dex_v1alpha1_client_generate_secret.yaml @@ -0,0 +1,12 @@ +apiVersion: dex.rax.io/v1alpha1 +kind: Client +metadata: + labels: + name: bob-client +spec: + name: bob + secretName: bobs-secret + generateSecret: true + redirectURIs: + - http://localhost:8080 + - https://some.service.example.com/ diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index b337cda5f..ad50210b7 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -24,8 +24,9 @@ import ( "github.com/go-logr/logr" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" dexmgr "github.com/rackerlabs/understack/go/dexop/dex" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -106,9 +107,28 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if clientSpec.Spec.SecretNamespace == "" { clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace } + // read existing or generate a secret value, err := r.readSecret(ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) if err != nil { - reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) + if errors.IsNotFound(err) && clientSpec.Spec.GenerateSecret { + secret, err := r.writeSecret(ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace, "ABRACADAXRA") + if err != nil { + reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) + return ctrl.Result{}, err + } + + if secret.Data["secret"] == nil { + reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) + } + value = string(secret.Data["secret"]) + ctrl.SetControllerReference(clientSpec, secret, r.Scheme) + if err = r.Update(ctx, secret); err != nil { + return ctrl.Result{}, err + } + } else { + reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) + return ctrl.Result{}, err + } } clientSpec.Spec.SecretValue = value } @@ -162,3 +182,21 @@ func (r *ClientReconciler) readSecret(ctx context.Context, name, namespace strin } return "", fmt.Errorf("secret key not found") } + +func (r *ClientReconciler) writeSecret(ctx context.Context, name, namespace, value string) (*v1.Secret, error) { + secret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{"secret": []byte(value)}, + Type: "Opaque", + } + + err := r.Create(ctx, secret) + if err != nil { + return nil, err + } + return secret, nil +} From 7cf58050d60e52f2af722567a6775bec94d8873b Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 16 Apr 2025 18:22:51 +0100 Subject: [PATCH 11/37] dexop: generate secure secret values --- go/dexop/go.mod | 1 - go/dexop/go.sum | 2 + .../internal/controller/client_controller.go | 40 ++------------ .../internal/controller/secret_manager.go | 54 +++++++++++++++++++ 4 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 go/dexop/internal/controller/secret_manager.go diff --git a/go/dexop/go.mod b/go/dexop/go.mod index cbb7214ca..724bda872 100644 --- a/go/dexop/go.mod +++ b/go/dexop/go.mod @@ -59,7 +59,6 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/sethvargo/go-password v0.3.1 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect diff --git a/go/dexop/go.sum b/go/dexop/go.sum index c0ee9a785..4cc340b21 100644 --- a/go/dexop/go.sum +++ b/go/dexop/go.sum @@ -114,6 +114,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= +github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index ad50210b7..08ad51645 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -18,15 +18,12 @@ package controller import ( "context" - "fmt" "strings" "github.com/go-logr/logr" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" dexmgr "github.com/rackerlabs/understack/go/dexop/dex" - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -103,15 +100,16 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } // if secretName specified, read the secret + secretmgr := new(SecretManager) if clientSpec.Spec.SecretName != "" { if clientSpec.Spec.SecretNamespace == "" { clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace } // read existing or generate a secret - value, err := r.readSecret(ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + value, err := secretmgr.readSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) if err != nil { if errors.IsNotFound(err) && clientSpec.Spec.GenerateSecret { - secret, err := r.writeSecret(ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace, "ABRACADAXRA") + secret, err := secretmgr.generateSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) if err != nil { reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) return ctrl.Result{}, err @@ -168,35 +166,3 @@ func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha } return nil } - -func (r *ClientReconciler) readSecret(ctx context.Context, name, namespace string) (string, error) { - secret := &v1.Secret{} - - err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, secret) - if err != nil { - return "", err - } - - if value, ok := secret.Data["secret"]; ok { - return string(value), nil - } - return "", fmt.Errorf("secret key not found") -} - -func (r *ClientReconciler) writeSecret(ctx context.Context, name, namespace, value string) (*v1.Secret, error) { - secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Data: map[string][]byte{"secret": []byte(value)}, - Type: "Opaque", - } - - err := r.Create(ctx, secret) - if err != nil { - return nil, err - } - return secret, nil -} diff --git a/go/dexop/internal/controller/secret_manager.go b/go/dexop/internal/controller/secret_manager.go new file mode 100644 index 000000000..8f26b35fc --- /dev/null +++ b/go/dexop/internal/controller/secret_manager.go @@ -0,0 +1,54 @@ +package controller + +import ( + "context" + "fmt" + + "github.com/sethvargo/go-password/password" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type SecretManager struct { +} + +func (s SecretManager) readSecret(r *ClientReconciler, ctx context.Context, name, namespace string) (string, error) { + secret := &v1.Secret{} + + err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, secret) + if err != nil { + return "", err + } + + if value, ok := secret.Data["secret"]; ok { + return string(value), nil + } + return "", fmt.Errorf("secret key not found") +} + +func (s SecretManager) writeSecret(r *ClientReconciler, ctx context.Context, name, namespace, value string) (*v1.Secret, error) { + secret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{"secret": []byte(value)}, + Type: "Opaque", + } + + err := r.Create(ctx, secret) + if err != nil { + return nil, err + } + return secret, nil +} + +func (s SecretManager) generateSecret(r *ClientReconciler, ctx context.Context, name, namespace string) (*v1.Secret, error) { + res, err := password.Generate(48, 10, 10, false, false) + if err != nil { + return nil, err + } + return s.writeSecret(r, ctx, name, namespace, res) +} From 4f16a05659507c3dbc21e3d061608d64550ee15d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 16 Apr 2025 19:33:33 +0100 Subject: [PATCH 12/37] dexop: refactor Reconcile part 1 --- .../internal/controller/client_controller.go | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 08ad51645..88377f261 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -56,21 +56,22 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr _ = log.FromContext(ctx) reqLogger := ctrl.Log.WithValues("client", req.NamespacedName) - mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "./grpc_ca.crt", "./grpc_client.key", "./grpc_client.crt") + mgr, err := r.newDexManager() if err != nil { - ctrl.Log.Error(err, "While getting the DexManager") return ctrl.Result{}, err } - clientSpec := &dexv1alpha1.Client{} - if err := r.Get(ctx, req.NamespacedName, clientSpec); err != nil { - if errors.IsNotFound(err) { - return ctrl.Result{}, nil - } + clientSpec, err := r.getClientSpec(ctx, req.NamespacedName) + if err != nil { return ctrl.Result{}, err } - reqLogger.Info("reconciling Client") + // resource was deleted but it is being finalized + if clientSpec == nil { + return ctrl.Result{}, nil + } + + reqLogger.Info("Reconciling Client") // delete if no longer needed deleteRequested := clientSpec.GetDeletionTimestamp() != nil @@ -159,6 +160,8 @@ func (r *ClientReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +// finalizeDeletion removes an OAuth2 client from Dex when the corresponding Client resource is deleted. +// It uses the DexManager to send a request to Dex over gRPC to delete the client. func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha1.Client, mgr *dexmgr.DexManager) error { reqLogger.Info("Client is being removed") if _, err := mgr.RemoveOauth2Client(c.Spec.Name); err != nil { @@ -166,3 +169,22 @@ func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha } return nil } + +func (r *ClientReconciler) newDexManager() (*dexmgr.DexManager, error) { + mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "./grpc_ca.crt", "./grpc_client.key", "./grpc_client.crt") + if err != nil { + ctrl.Log.Error(err, "While getting the DexManager") + } + return mgr, err +} + +func (r *ClientReconciler) getClientSpec(ctx context.Context, namespacedName types.NamespacedName) (*dexv1alpha1.Client, error) { + clientSpec := &dexv1alpha1.Client{} + if err := r.Get(ctx, namespacedName, clientSpec); err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return clientSpec, nil +} From 2a2062b93424a4e08f9af2d677031de4d73f8b30 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 17 Apr 2025 10:01:48 +0100 Subject: [PATCH 13/37] dexop: improve tests --- go/dexop/internal/controller/client_controller.go | 3 ++- .../internal/controller/client_controller_test.go | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 88377f261..97c5e33a9 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -25,6 +25,7 @@ import ( dexmgr "github.com/rackerlabs/understack/go/dexop/dex" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -171,7 +172,7 @@ func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha } func (r *ClientReconciler) newDexManager() (*dexmgr.DexManager, error) { - mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "./grpc_ca.crt", "./grpc_client.key", "./grpc_client.crt") + mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "/home/skrobul/devel/understack/go/dexop/grpc_ca.crt", "/home/skrobul/devel/understack/go/dexop/grpc_client.key", "/home/skrobul/devel/understack/go/dexop/grpc_client.crt") if err != nil { ctrl.Log.Error(err, "While getting the DexManager") } diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index 207c14b7b..9d3de96a3 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -38,7 +38,7 @@ var _ = Describe("Client Controller", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + Namespace: "default", } client := &dexv1alpha1.Client{} @@ -51,14 +51,19 @@ var _ = Describe("Client Controller", func() { Name: resourceName, Namespace: "default", }, - // TODO(user): Specify other spec details if needed. + Spec: dexv1alpha1.ClientSpec{ + Name: "fred-client", + SecretName: "freds-secret", + GenerateSecret: true, + RedirectURIs: []string{"http://localhost:8080", "https://some.service.example.com/callback"}, + LogoUrl: "http://logoserver.local/xyz.png", + }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } }) AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. resource := &dexv1alpha1.Client{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) From 5270e93a61fddb6f3a4b0cbf5c98af9db572dbca Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 23 Apr 2025 16:08:34 +0100 Subject: [PATCH 14/37] dexop: add more comprehensive tests --- .../controller/client_controller_test.go | 212 +++++++++++++++++- 1 file changed, 205 insertions(+), 7 deletions(-) diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index 9d3de96a3..5bfba6004 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -18,9 +18,12 @@ package controller import ( "context" + "fmt" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -28,19 +31,31 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + dexmgr "github.com/rackerlabs/understack/go/dexop/dex" ) -var _ = Describe("Client Controller", func() { - Context("When reconciling a resource", func() { - const resourceName = "test-resource" +func newDexMgr() (*dexmgr.DexManager, error) { + mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "/home/skrobul/devel/understack/go/dexop/grpc_ca.crt", "/home/skrobul/devel/understack/go/dexop/grpc_client.key", "/home/skrobul/devel/understack/go/dexop/grpc_client.crt") + if err != nil { + return nil, fmt.Errorf("While getting the DexManager") + } + return mgr, err +} - ctx := context.Background() +var _ = Describe("Client Controller", func() { + const resourceName = "test-resource" + const secretName = "freds-secret" + ctx := context.Background() + Context("When reconciling a resource", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", } + typesNamespacedSecretName := types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: secretName} client := &dexv1alpha1.Client{} + dex, err := newDexMgr() + Expect(err).NotTo(HaveOccurred()) BeforeEach(func() { By("creating the custom resource for the Kind Client") @@ -53,7 +68,7 @@ var _ = Describe("Client Controller", func() { }, Spec: dexv1alpha1.ClientSpec{ Name: "fred-client", - SecretName: "freds-secret", + SecretName: secretName, GenerateSecret: true, RedirectURIs: []string{"http://localhost:8080", "https://some.service.example.com/callback"}, LogoUrl: "http://logoserver.local/xyz.png", @@ -65,11 +80,43 @@ var _ = Describe("Client Controller", func() { AfterEach(func() { resource := &dexv1alpha1.Client{} + By("lookup of the existing resource") err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) By("Cleanup the specific resource instance Client") Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + // we need to run reconciler again to process finalizer + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, &dexv1alpha1.Client{}) + return errors.IsNotFound(err) + }, 15*time.Second, 500*time.Millisecond).Should(BeTrue()) + }) + + It("should create a secret with a non-empty value", func() { + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + secretObj := &v1.Secret{} + k8sClient.Get(ctx, types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: secretName}, secretObj) + Expect(len(secretObj.Data["secret"])).To(Equal(48)) + Expect(err).NotTo(HaveOccurred()) }) It("should successfully reconcile the resource", func() { By("Reconciling the created resource") @@ -82,8 +129,159 @@ var _ = Describe("Client Controller", func() { NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + It("should update the password after secret changes", func() { + By("Reconciling the created resource") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + secretObj := &v1.Secret{} + Expect(k8sClient.Get(ctx, typesNamespacedSecretName, secretObj)).To(Succeed()) + secretObj.Data["secret"] = []byte("newSecret") + + By("reconcile after changing the secret") + Expect(k8sClient.Update(ctx, secretObj)).To(Succeed()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + By("verifying if the Dex side was updated") + dexclient, err := dex.GetOauth2Client("fred-client") + Expect(err).NotTo(HaveOccurred()) + Expect(dexclient.Client.Secret).To(Equal("newSecret")) + }) + It("updates RedirectURIs", func() { + By("updating redirectURIs in dex") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &dexv1alpha1.Client{} + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.Spec.RedirectURIs = []string{"https://new.redirect.local"} + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + By("reconciling again") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + By("checking if the RedirectURIs have been updated in Dex") + dexclient, err := dex.GetOauth2Client("fred-client") + Expect(err).NotTo(HaveOccurred()) + Expect(dexclient.Client.RedirectUris).To(Equal([]string{"https://new.redirect.local"})) + }) + + It("creates the secret if it is missing", func() { + By("reconciling the first time") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + + By("deleting the Secret") + secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace }} + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + Expect(k8sClient.Get(ctx, typesNamespacedSecretName, secret)).NotTo(Succeed()) + + By("doing another round of reconciliation") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + By("checking if the Secret has been recreated") + newSecret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace }} + Expect(k8sClient.Get(ctx, typesNamespacedSecretName, newSecret)) + }) + }) + + Context("pre-created Secret", func() { + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + dex, err := newDexMgr() + Expect(err).NotTo(HaveOccurred()) + It("copies the secret value", func() { + By("creating Client resource") + testSecretName := "test-secret-123" + resource := &dexv1alpha1.Client{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: dexv1alpha1.ClientSpec{ + Name: "fred-client", + SecretName: testSecretName, + GenerateSecret: false, + RedirectURIs: []string{"http://localhost:8080", "https://some.service.example.com/callback"}, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + By("creating secret") + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: "default" }, + Data: map[string][]byte{"secret": []byte("abc") }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + By("doing another round of reconciliation") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + dexclient, err := dex.GetOauth2Client("fred-client") + Expect(err).NotTo(HaveOccurred()) + Expect(dexclient.Client.Secret).To(Equal("abc")) + }) + + AfterEach(func() { + resource := &dexv1alpha1.Client{} + By("lookup of the existing resource") + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Client") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + // we need to run reconciler again to process finalizer + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, &dexv1alpha1.Client{}) + return errors.IsNotFound(err) + }, 15*time.Second, 500*time.Millisecond).Should(BeTrue()) }) }) }) From b40e40f9adac17373ec6860ed583ef7c580686dc Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 23 Apr 2025 18:25:04 +0100 Subject: [PATCH 15/37] dexop: implement update by recreate --- go/dexop/dex/client.go | 17 +++++++++++++---- .../internal/controller/client_controller.go | 11 +++++------ .../controller/client_controller_test.go | 9 ++++----- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index c55ab4cb1..35606b6f5 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -51,19 +51,27 @@ func (d *DexManager) GetOauth2Client(clientId string) (*dexapi.GetClientResp, er // Patches/updates the Oauth2 client -func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.UpdateClientResp, error) { +func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) error { existing, err := d.GetOauth2Client(clientSpec.Spec.Name) if err != nil { - return nil, err + return err } if existing == nil { - return nil, fmt.Errorf("oauth2 client id: %s does not exist in Dex", clientSpec.Spec.Name) + return fmt.Errorf("oauth2 client id: %s does not exist in Dex", clientSpec.Spec.Name) } if existing.Client.Secret != clientSpec.Spec.SecretValue || existing.Client.Public != clientSpec.Spec.Public { // dex does not support secret updates so it needs to be recreated + _, err = d.RemoveOauth2Client(clientSpec.Spec.Name) + if err != nil { + return err + } + _, err := d.CreateOauth2Client(clientSpec) + if err != nil { + return err + } } updateRequest := &dexapi.UpdateClientReq{ @@ -73,7 +81,8 @@ func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi LogoUrl: clientSpec.Spec.LogoUrl, Name: clientSpec.Spec.Name, } - return d.Client.UpdateClient(context.TODO(), updateRequest) + _, err = d.Client.UpdateClient(context.TODO(), updateRequest) + return err } func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexClient, error) { diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 97c5e33a9..a6c894d6a 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -46,11 +46,6 @@ type ClientReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Client object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -149,7 +144,11 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // update if existing != nil { reqLogger.Info("making an UpdateOauth2Client call") - mgr.UpdateOauth2Client(clientSpec) + err = mgr.UpdateOauth2Client(clientSpec) + if err != nil { + reqLogger.Error(err, "after UpdateOauth2Client") + return ctrl.Result{}, err + } } return ctrl.Result{}, nil } diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index 5bfba6004..8f589d18d 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -196,9 +196,8 @@ var _ = Describe("Client Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - By("deleting the Secret") - secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace }} + secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace}} Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) Expect(k8sClient.Get(ctx, typesNamespacedSecretName, secret)).NotTo(Succeed()) @@ -209,7 +208,7 @@ var _ = Describe("Client Controller", func() { Expect(err).NotTo(HaveOccurred()) By("checking if the Secret has been recreated") - newSecret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace }} + newSecret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace}} Expect(k8sClient.Get(ctx, typesNamespacedSecretName, newSecret)) }) }) @@ -240,8 +239,8 @@ var _ = Describe("Client Controller", func() { By("creating secret") secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: "default" }, - Data: map[string][]byte{"secret": []byte("abc") }, + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: "default"}, + Data: map[string][]byte{"secret": []byte("abc")}, } Expect(k8sClient.Create(ctx, secret)).To(Succeed()) From 060c53f07a47328646edb364cf7112001bc1dfcf Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 23 Apr 2025 18:32:30 +0100 Subject: [PATCH 16/37] dexop: update rbac --- go/dexop/config/rbac/role.yaml | 10 ++++++++++ go/dexop/internal/controller/client_controller.go | 1 + 2 files changed, 11 insertions(+) diff --git a/go/dexop/config/rbac/role.yaml b/go/dexop/config/rbac/role.yaml index 42737d6cb..719b99a74 100644 --- a/go/dexop/config/rbac/role.yaml +++ b/go/dexop/config/rbac/role.yaml @@ -4,6 +4,16 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - dex.rax.io resources: diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index a6c894d6a..00754cbe2 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -43,6 +43,7 @@ type ClientReconciler struct { // +kubebuilder:rbac:groups=dex.rax.io,resources=clients,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=dex.rax.io,resources=clients/status,verbs=get;update;patch // +kubebuilder:rbac:groups=dex.rax.io,resources=clients/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. From 39a04faf3712233b1a363b07e86ad8b83157496d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 23 Apr 2025 19:14:07 +0100 Subject: [PATCH 17/37] dexop: ability to configure certs --- go/dexop/cmd/main.go | 36 ++++++++++++++++- .../internal/controller/client_controller.go | 24 +++-------- .../controller/client_controller_test.go | 40 +++++++++++-------- 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/go/dexop/cmd/main.go b/go/dexop/cmd/main.go index 285ed021d..7c825773d 100644 --- a/go/dexop/cmd/main.go +++ b/go/dexop/cmd/main.go @@ -36,6 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" + dexmgr "github.com/rackerlabs/understack/go/dexop/dex" "github.com/rackerlabs/understack/go/dexop/internal/controller" // +kubebuilder:scaffold:imports ) @@ -59,6 +60,7 @@ func main() { var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) + dexConf := new(DexConfig) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -69,6 +71,14 @@ func main() { "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.StringVar(&dexConf.Address, "dex-host", "127.0.0.1:5557", + "The address:port of the Dex GRPC API to manage.") + flag.StringVar(&dexConf.CAPath, "dex-ca-path", "./grpc_ca.crt", + "Path to a CA certificate for Dex") + flag.StringVar(&dexConf.ClientCertPath, "dex-cert-path", "./grpc_client.crt", + "Path to the client certificate for Dex API") + flag.StringVar(&dexConf.ClientKeyPath, "dex-key-path", "./grpc_client.key", + "Path to the client key for Dex API") opts := zap.Options{ Development: true, } @@ -144,9 +154,16 @@ func main() { os.Exit(1) } + dexMgr, err := newDexManager(dexConf) + if err != nil { + setupLog.Error(err, "unable to create dex manager") + os.Exit(1) + } + if err = (&controller.ClientReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + DexManager: dexMgr, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Client") os.Exit(1) @@ -168,3 +185,18 @@ func main() { os.Exit(1) } } + +type DexConfig struct { + Address string + CAPath string + ClientKeyPath string + ClientCertPath string +} + +func newDexManager(config *DexConfig) (*dexmgr.DexManager, error) { + mgr, err := dexmgr.NewDexManager(config.Address, config.CAPath, config.ClientKeyPath, config.ClientCertPath) + if err != nil { + setupLog.Error(err, "While getting the DexManager") + } + return mgr, err +} diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 00754cbe2..5fd9912aa 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -37,7 +37,8 @@ const dexFinalizer = "dex.rax.io/finalizer" // ClientReconciler reconciles a Client object type ClientReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + DexManager *dexmgr.DexManager } // +kubebuilder:rbac:groups=dex.rax.io,resources=clients,verbs=get;list;watch;create;update;patch;delete @@ -53,11 +54,6 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr _ = log.FromContext(ctx) reqLogger := ctrl.Log.WithValues("client", req.NamespacedName) - mgr, err := r.newDexManager() - if err != nil { - return ctrl.Result{}, err - } - clientSpec, err := r.getClientSpec(ctx, req.NamespacedName) if err != nil { return ctrl.Result{}, err @@ -74,7 +70,7 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr deleteRequested := clientSpec.GetDeletionTimestamp() != nil if deleteRequested { if controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { - if err := r.finalizeDeletion(reqLogger, clientSpec, mgr); err != nil { + if err := r.finalizeDeletion(reqLogger, clientSpec, r.DexManager); err != nil { return ctrl.Result{}, err } @@ -129,11 +125,11 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr clientSpec.Spec.SecretValue = value } - existing, err := mgr.GetOauth2Client(clientSpec.Spec.Name) + existing, err := r.DexManager.GetOauth2Client(clientSpec.Spec.Name) if err != nil { if strings.Contains(strings.ToLower(err.Error()), "not found") { ctrl.Log.Info("Client does not exist in Dex. Creating one.", "name", clientSpec.Spec.Name) - if _, err = mgr.CreateOauth2Client(clientSpec); err != nil { + if _, err = r.DexManager.CreateOauth2Client(clientSpec); err != nil { reqLogger.Error(err, "Unable to create client in dex") return ctrl.Result{}, err } @@ -145,7 +141,7 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // update if existing != nil { reqLogger.Info("making an UpdateOauth2Client call") - err = mgr.UpdateOauth2Client(clientSpec) + err = r.DexManager.UpdateOauth2Client(clientSpec) if err != nil { reqLogger.Error(err, "after UpdateOauth2Client") return ctrl.Result{}, err @@ -171,14 +167,6 @@ func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha return nil } -func (r *ClientReconciler) newDexManager() (*dexmgr.DexManager, error) { - mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "/home/skrobul/devel/understack/go/dexop/grpc_ca.crt", "/home/skrobul/devel/understack/go/dexop/grpc_client.key", "/home/skrobul/devel/understack/go/dexop/grpc_client.crt") - if err != nil { - ctrl.Log.Error(err, "While getting the DexManager") - } - return mgr, err -} - func (r *ClientReconciler) getClientSpec(ctx context.Context, namespacedName types.NamespacedName) (*dexv1alpha1.Client, error) { clientSpec := &dexv1alpha1.Client{} if err := r.Get(ctx, namespacedName, clientSpec); err != nil { diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index 8f589d18d..10d558589 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -88,8 +88,9 @@ var _ = Describe("Client Controller", func() { Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) // we need to run reconciler again to process finalizer controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, @@ -104,8 +105,9 @@ var _ = Describe("Client Controller", func() { It("should create a secret with a non-empty value", func() { controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ @@ -121,8 +123,9 @@ var _ = Describe("Client Controller", func() { It("should successfully reconcile the resource", func() { By("Reconciling the created resource") controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ @@ -133,8 +136,9 @@ var _ = Describe("Client Controller", func() { It("should update the password after secret changes", func() { By("Reconciling the created resource") controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ @@ -160,8 +164,9 @@ var _ = Describe("Client Controller", func() { It("updates RedirectURIs", func() { By("updating redirectURIs in dex") controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, @@ -188,8 +193,9 @@ var _ = Describe("Client Controller", func() { It("creates the secret if it is missing", func() { By("reconciling the first time") controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, @@ -246,8 +252,9 @@ var _ = Describe("Client Controller", func() { By("doing another round of reconciliation") controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, @@ -269,8 +276,9 @@ var _ = Describe("Client Controller", func() { Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) // we need to run reconciler again to process finalizer controllerReconciler := &ClientReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, } _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, From bbe91712e6238b09eb9ec76a41d6cec10cddc41b Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 24 Apr 2025 16:08:57 +0100 Subject: [PATCH 18/37] dexop: use dedicated instance of Dex for testing Up till now all tests required the Dex server to be already running on the developer's machine. It was also assumed that the server will be preconfigured for the gRPC communication. This required the appropriate certificates to be generated, configured in Dex and copied over to the project directory. This commit removes that requirement altogether by automatically starting "dex serve" with appropriate configuration and shutting it down when the tests complete. --- go/dexop/dex/client.go | 10 ++++ .../controller/client_controller_test.go | 13 +--- go/dexop/internal/controller/suite_test.go | 60 +++++++++++++++++-- go/dexop/test/support/testdex_config.yaml | 15 +++++ 4 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 go/dexop/test/support/testdex_config.yaml diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index 35606b6f5..c3ab547c8 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -11,6 +11,7 @@ import ( dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" ) type DexManager struct { @@ -113,6 +114,15 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC return dexapi.NewDexClient(conn), nil } +func NewInsecureTestManager(grpcAddr string) (*DexManager, error) { + // Establish gRPC connection for tests to use + grpcConn, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + return &DexManager{Client: dexapi.NewDexClient(grpcConn)}, nil +} + func NewDexManager(host, caCert, clientKey, clientCert string) (*DexManager, error) { client, err := newDexClient(host, caCert, clientKey, clientCert) if err != nil { diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index 10d558589..efd3eb1ec 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -18,7 +18,6 @@ package controller import ( "context" - "fmt" "time" . "github.com/onsi/ginkgo/v2" @@ -34,13 +33,7 @@ import ( dexmgr "github.com/rackerlabs/understack/go/dexop/dex" ) -func newDexMgr() (*dexmgr.DexManager, error) { - mgr, err := dexmgr.NewDexManager("127.0.0.1:5557", "/home/skrobul/devel/understack/go/dexop/grpc_ca.crt", "/home/skrobul/devel/understack/go/dexop/grpc_client.key", "/home/skrobul/devel/understack/go/dexop/grpc_client.crt") - if err != nil { - return nil, fmt.Errorf("While getting the DexManager") - } - return mgr, err -} +const testDexHostAddr = "127.0.0.1:15557" var _ = Describe("Client Controller", func() { const resourceName = "test-resource" @@ -54,7 +47,7 @@ var _ = Describe("Client Controller", func() { } typesNamespacedSecretName := types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: secretName} client := &dexv1alpha1.Client{} - dex, err := newDexMgr() + dex, err := dexmgr.NewInsecureTestManager(testDexHostAddr) Expect(err).NotTo(HaveOccurred()) BeforeEach(func() { @@ -224,7 +217,7 @@ var _ = Describe("Client Controller", func() { Name: resourceName, Namespace: "default", } - dex, err := newDexMgr() + dex, err := dexmgr.NewInsecureTestManager(testDexHostAddr) Expect(err).NotTo(HaveOccurred()) It("copies the secret value", func() { By("creating Client resource") diff --git a/go/dexop/internal/controller/suite_test.go b/go/dexop/internal/controller/suite_test.go index 2fb146e55..3fd0609a4 100644 --- a/go/dexop/internal/controller/suite_test.go +++ b/go/dexop/internal/controller/suite_test.go @@ -19,12 +19,17 @@ package controller import ( "context" "fmt" + "os" + "os/exec" "path/filepath" "runtime" "testing" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -40,11 +45,14 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment -var ctx context.Context -var cancel context.CancelFunc +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + dexCmd *exec.Cmd +) func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -77,6 +85,31 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) + // Start local Dex server + By("starting local Dex instance") + dexConfigPath := "../../test/support/testdex_config.yaml" + dexBinaryName := "dex" + + _, err = os.Stat(dexConfigPath) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Dex test config file %s not found", dexConfigPath)) + _, err = exec.LookPath("dex") + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("dex is not available in the $PATH")) + + dexCmd = exec.Command(dexBinaryName, "serve", dexConfigPath) + dexCmd.Stdout = GinkgoWriter + dexCmd.Stderr = GinkgoWriter + + err = dexCmd.Start() + Expect(err).NotTo(HaveOccurred(), "failed to start Dex command") + + // Wait for Dex gRPC server to be ready + grpcAddr := testDexHostAddr + Eventually(func(g Gomega) { + conn, dialErr := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), grpc.WithTimeout(1*time.Second)) + g.Expect(dialErr).NotTo(HaveOccurred(), "waiting for Dex gRPC server to be ready") + Expect(conn.Close()).To(Succeed()) + }, 10*time.Second, 1*time.Second).Should(Succeed()) + err = dexv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) @@ -85,11 +118,26 @@ var _ = BeforeSuite(func() { k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) - }) var _ = AfterSuite(func() { By("tearing down the test environment") + + // --- Stop Dex Server --- + By("stopping Dex server") + if dexCmd != nil && dexCmd.Process != nil { + if err := dexCmd.Process.Kill(); err != nil { + GinkgoWriter.Println("failed to kill Dex process: %v", err) + } + // Wait for the process to exit to avoid zombies + _, err := dexCmd.Process.Wait() + if err != nil && err.Error() != "signal: killed" { + GinkgoWriter.Println("error waiting for Dex process to exit: %v", err) + + } + GinkgoWriter.Println("Dex server stopped.") + } + cancel() err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) diff --git a/go/dexop/test/support/testdex_config.yaml b/go/dexop/test/support/testdex_config.yaml new file mode 100644 index 000000000..9317005aa --- /dev/null +++ b/go/dexop/test/support/testdex_config.yaml @@ -0,0 +1,15 @@ +issuer: http://127.0.0.1:15556/dex # The URL clients will use to connect to Dex +storage: + type: memory +web: + http: 127.0.0.1:15556 + +grpc: + addr: 127.0.0.1:15557 + # don't do that in production + plainText: true + +connectors: +- type: mockCallback + id: mock + name: Example From b8f7509eba88bfad456897f69a852fe674302af1 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 24 Apr 2025 16:41:26 +0100 Subject: [PATCH 19/37] dexop: fix most linting issues grpc.Dial deprecation will be done in separate commit in case w need to revert. --- go/dexop/dex/client.go | 2 +- go/dexop/internal/controller/client_controller.go | 5 ++++- go/dexop/internal/controller/client_controller_test.go | 2 +- go/dexop/internal/controller/suite_test.go | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index c3ab547c8..f14379535 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -92,7 +92,7 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC if err != nil { return nil, fmt.Errorf("invalid CA crt file: %s", caPath) } - if cPool.AppendCertsFromPEM(caCert) != true { + if !cPool.AppendCertsFromPEM(caCert) { return nil, fmt.Errorf("failed to parse CA crt") } diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 5fd9912aa..e42b67bdd 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -113,7 +113,10 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) } value = string(secret.Data["secret"]) - ctrl.SetControllerReference(clientSpec, secret, r.Scheme) + if err = ctrl.SetControllerReference(clientSpec, secret, r.Scheme); err != nil { + return ctrl.Result{}, err + } + if err = r.Update(ctx, secret); err != nil { return ctrl.Result{}, err } diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index efd3eb1ec..931362dba 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -109,7 +109,7 @@ var _ = Describe("Client Controller", func() { Expect(err).NotTo(HaveOccurred()) secretObj := &v1.Secret{} - k8sClient.Get(ctx, types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: secretName}, secretObj) + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: secretName}, secretObj) Expect(len(secretObj.Data["secret"])).To(Equal(48)) Expect(err).NotTo(HaveOccurred()) }) diff --git a/go/dexop/internal/controller/suite_test.go b/go/dexop/internal/controller/suite_test.go index 3fd0609a4..3f8a350e0 100644 --- a/go/dexop/internal/controller/suite_test.go +++ b/go/dexop/internal/controller/suite_test.go @@ -93,7 +93,7 @@ var _ = BeforeSuite(func() { _, err = os.Stat(dexConfigPath) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Dex test config file %s not found", dexConfigPath)) _, err = exec.LookPath("dex") - Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("dex is not available in the $PATH")) + Expect(err).NotTo(HaveOccurred(), "dex is not available in the $PATH") dexCmd = exec.Command(dexBinaryName, "serve", dexConfigPath) dexCmd.Stdout = GinkgoWriter From 8317aa28c7d8e2b4abeaf8cbfa318e7c67721cae Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 24 Apr 2025 16:53:19 +0100 Subject: [PATCH 20/37] dexop: migrate from deprecated grpc.Dial --- go/dexop/dex/client.go | 4 ++-- go/dexop/internal/controller/suite_test.go | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index f14379535..43e8c4183 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -107,7 +107,7 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC } creds := credentials.NewTLS(clientTLSConfig) - conn, err := grpc.Dial(hostAndPort, grpc.WithTransportCredentials(creds)) + conn, err := grpc.NewClient(hostAndPort, grpc.WithTransportCredentials(creds)) if err != nil { return nil, fmt.Errorf("dial: %v", err) } @@ -116,7 +116,7 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC func NewInsecureTestManager(grpcAddr string) (*DexManager, error) { // Establish gRPC connection for tests to use - grpcConn, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + grpcConn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, err } diff --git a/go/dexop/internal/controller/suite_test.go b/go/dexop/internal/controller/suite_test.go index 3f8a350e0..bb2724520 100644 --- a/go/dexop/internal/controller/suite_test.go +++ b/go/dexop/internal/controller/suite_test.go @@ -29,6 +29,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials/insecure" "k8s.io/client-go/kubernetes/scheme" @@ -38,6 +39,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + dexapi "github.com/dexidp/dex/api/v2" dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -105,9 +107,15 @@ var _ = BeforeSuite(func() { // Wait for Dex gRPC server to be ready grpcAddr := testDexHostAddr Eventually(func(g Gomega) { - conn, dialErr := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), grpc.WithTimeout(1*time.Second)) + conn, dialErr := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) g.Expect(dialErr).NotTo(HaveOccurred(), "waiting for Dex gRPC server to be ready") - Expect(conn.Close()).To(Succeed()) + state := conn.GetState() + g.Expect(state).To(Equal(connectivity.Idle)) + result := conn.WaitForStateChange(ctx, connectivity.Ready) + g.Expect(result).To(Equal(true)) + err = conn.Invoke(ctx, "/api.Dex/ListPasswords", nil, new(dexapi.ListPasswordResp), grpc.StaticMethod()) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(conn.Close()).To(Succeed()) }, 10*time.Second, 1*time.Second).Should(Succeed()) err = dexv1alpha1.AddToScheme(scheme.Scheme) @@ -133,7 +141,6 @@ var _ = AfterSuite(func() { _, err := dexCmd.Process.Wait() if err != nil && err.Error() != "signal: killed" { GinkgoWriter.Println("error waiting for Dex process to exit: %v", err) - } GinkgoWriter.Println("Dex server stopped.") } From eff30b6bf33e5ef67ad3a113151a371f72c85782 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 24 Apr 2025 20:30:54 +0100 Subject: [PATCH 21/37] dexop: build container with go 1.23 --- go/dexop/Dockerfile | 3 ++- go/dexop/Makefile | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go/dexop/Dockerfile b/go/dexop/Dockerfile index a48973ee7..188b39d19 100644 --- a/go/dexop/Dockerfile +++ b/go/dexop/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.22 AS builder +FROM golang:1.23 AS builder ARG TARGETOS ARG TARGETARCH @@ -15,6 +15,7 @@ RUN go mod download COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/controller/ internal/controller/ +COPY dex/ dex/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/go/dexop/Makefile b/go/dexop/Makefile index 4fb40e51e..86e02fa6a 100644 --- a/go/dexop/Makefile +++ b/go/dexop/Makefile @@ -28,8 +28,8 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) # This variable is used to construct full image tags for bundle and catalog images. # # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both -# rax.io/dexop-bundle:$VERSION and rax.io/dexop-catalog:$VERSION. -IMAGE_TAG_BASE ?= rax.io/dexop +# ghcr.io/understack/dexop-bundle:$VERSION and ghcr.io/understack/dexop-catalog:$VERSION. +IMAGE_TAG_BASE ?= ghcr.io/understack/dexop # BUNDLE_IMG defines the image:tag used for the bundle. # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) From 1983fc06b2c3265f34d83850f7792555df697982 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 10:14:46 +0100 Subject: [PATCH 22/37] dexop: add github container builds workflow --- .github/workflows/build-dexop.yaml | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/build-dexop.yaml diff --git a/.github/workflows/build-dexop.yaml b/.github/workflows/build-dexop.yaml new file mode 100644 index 000000000..ab8f5edfa --- /dev/null +++ b/.github/workflows/build-dexop.yaml @@ -0,0 +1,48 @@ +--- +name: build-dexop-images + +on: + workflow_dispatch: + pull_request: + paths: + - "go/dexop/**" + push: + tags: + - dexop-v* + paths: + - "go/dexop/**" + merge_group: + types: [checks_requested] + +jobs: + build-ghcr-registry: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + + - name: Login to ghcr.io + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: "ghcr.io" + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract tag name + id: extract_tag + run: echo "tag=${GITHUB_REF#refs/tags/dexop-v}" >> $GITHUB_OUTPUT + + - name: Build and deploy Dexop image + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + with: + context: go/dexop/ + file: go/dexop/Dockerfile + # push for all main branch commits + push: ${{ github.event_name != 'pull_request' }} + tags: ghcr.io/${{ github.repository }}/dexop:latest,ghcr.io/${{ github.repository }}/dexop:${{ steps.extract_tag.outputs.tag }} + labels: | + org.opencontainers.image.version=${{ steps.extract_tag.outputs.tag }} From a85c7647ebf6decf1a1a781916f487d89519f49b Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 12:01:38 +0100 Subject: [PATCH 23/37] dexop: update the README --- go/dexop/README.md | 142 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 130 insertions(+), 12 deletions(-) diff --git a/go/dexop/README.md b/go/dexop/README.md index 53fd41e19..efa6c7157 100644 --- a/go/dexop/README.md +++ b/go/dexop/README.md @@ -1,27 +1,142 @@ # dexop -// TODO(user): Add simple overview of use/purpose -## Description -// TODO(user): An in-depth paragraph about your project and overview of use +Kubernetes operator to manage [DEX][dex] Oauth2 clients. -## Getting Started +[dex]: + +The `dexop` operator is designed to simplify the management of OAuth2 clients +in Dex, a popular open-source identity and authentication service. By +leveraging Kubernetes resources, `dexop` allows for more streamlined control +over client lifecycles. + +## Supported Operations + +The following operations are supported by the `dexop` operator: + +- **Client Management**: Create, update, and delete `Client`s in Dex. +- **Password Management**: Update passwords for existing clients, or + automatically generate and store random passwords as Kubernetes secrets. +- **Secret Integration**: Read passwords from pre-existing `Secret`s and + synchronize them with Dex. +- **Redirect URI Updates**: Update `redirectURIs` for existing `Client`s, +ensuring flexibility in handling client redirects. + +## Example Usage + +### Generate password + +```yaml +--- +apiVersion: dex.rax.io/v1alpha1 +kind: Client +metadata: + name: bob-client +spec: + name: bob + secretName: bobs-secret + generateSecret: true + redirectURIs: + - http://localhost:8080 + - https://some.service.example.com/ +``` + +The snippet above will contact configured Dex instance over GRPC API, create an +Oauth2 client named `bob`. The created client will have a randomly generated +password. The value of the password will be stored as a `Secret` and can be +read by any application running in the same Kubernetes namespace, providing it +has appropriate permissions. + +### Pre-existing Secret + +Sometimes, we may want to explicitly set the password or simply control it +externally. This can be done by creating the Secret and providing its name. + +```yaml +--- +apiVersion: dex.rax.io/v1alpha1 +kind: Client +metadata: + name: fred-client +spec: + name: fred + secretName: my-secret + generateSecret: false # defaults to false + redirectURIs: + - https://freds.website.com/oauth2/callback +``` + +### Secrets in different namespace + +By default, the `dexop` assumes that Secrets will be placed in the same +namespace as the `Client` resource. However, in some of the setups it is +required to use different namespace. This can be configured with +`secretNamespace` attribute. + +```yaml +apiVersion: dex.rax.io/v1alpha1 +kind: Client +metadata: + labels: + name: keystone-client +spec: + name: keystone + secretName: keystone-secret + secretNamespace: openstack + generateSecret: true + redirectURIs: + - http://localhost:8080 + - https://keystone.openstack.svc.cluster.local/web/oauth2/callback +``` + +## Installation + +This operator is distributed through a Helm chart located in the +`go/dexop/helm` directory. + +- Adjust + [values.yaml](https://github.com/rackerlabs/understack/tree/main/go/dexop/helm/values.yaml) +for your needs. +- Create a Secret with credentials for `dexop` to access the `DEX` API. Example setup: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: dexop-dex-client +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0.... + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tC.... + ca.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL.... +``` + +If deploying without helm, the container image is available [here](https://github.com/rackerlabs/understack/pkgs/container/understack%2Fdexop). + +## Development ### Prerequisites -- go version v1.22.0+ + +- go version v1.23.0+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. +Make sure that your kubectl is not connected to a production cluster, ideally +use a local cluster like [kind](https://kind.sigs.k8s.io/). + ### To Deploy on the cluster + **Build and push your image to the location specified by `IMG`:** ```sh make docker-build docker-push IMG=/dexop:tag ``` -**NOTE:** This image ought to be published in the personal registry you specified. -And it is required to have access to pull the image from the working environment. -Make sure you have the proper permission to the registry if the above commands don’t work. +**NOTE:** This image ought to be published in the personal registry you +specified. And it is required to have access to pull the image from the working +environment. Make sure you have the proper permission to the registry if the +above commands don’t work. **Install the CRDs into the cluster:** @@ -48,6 +163,7 @@ kubectl apply -k config/samples/ >**NOTE**: Ensure that the samples has default values to test it out. ### To Uninstall + **Delete the instances (CRs) from the cluster:** ```sh @@ -81,16 +197,18 @@ file in the dist directory. This file contains all the resources built with Kustomize, which are necessary to install this project without its dependencies. -2. Using the installer +1. Using the installer -Users can just run kubectl apply -f to install the project, i.e.: +Users can just run `kubectl apply -f ` to install the +project, i.e.: ```sh kubectl apply -f https://raw.githubusercontent.com//dexop//dist/install.yaml ``` +The project can be installed through Helm as well (described in the Install section) + ## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project **NOTE:** Run `make help` for more information on all potential `make` targets @@ -98,7 +216,7 @@ More information can be found via the [Kubebuilder Documentation](https://book.k ## License -Copyright 2025. +Copyright 2025 Rackspace Technology. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 18e9cd3d33533afa688f04effd81d7e5b09a1583 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 13:28:51 +0100 Subject: [PATCH 24/37] dexop: add autogenerated install --- go/dexop/dist/install.yaml | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 go/dexop/dist/install.yaml diff --git a/go/dexop/dist/install.yaml b/go/dexop/dist/install.yaml new file mode 100644 index 000000000..b1f6afe84 --- /dev/null +++ b/go/dexop/dist/install.yaml @@ -0,0 +1,59 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: dexop + control-plane: controller-manager + name: dexop-controller-manager + namespace: dexop-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + command: + - /manager + image: controller:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: dexop-controller-manager + terminationGracePeriodSeconds: 10 From 6fe521f19527a2c5a1b1ed3a2e9cf0da6f6cd954 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 13:32:06 +0100 Subject: [PATCH 25/37] dexop: add Helm chart This deploys basic version of the operator. --- go/dexop/helm/.helmignore | 23 ++++ go/dexop/helm/Chart.yaml | 6 + go/dexop/helm/crds/clients.yaml | 73 +++++++++++ go/dexop/helm/templates/NOTES.txt | 11 ++ go/dexop/helm/templates/_helpers.tpl | 62 +++++++++ go/dexop/helm/templates/clusterrole.yaml.tpl | 124 ++++++++++++++++++ .../templates/clusterrolebinding.yaml.tpl | 30 +++++ go/dexop/helm/templates/deployment.yaml.tpl | 69 ++++++++++ go/dexop/helm/templates/rolebinding.yaml.tpl | 16 +++ go/dexop/helm/templates/roles.yaml.tpl | 40 ++++++ go/dexop/helm/templates/service.yaml.tpl | 15 +++ .../helm/templates/serviceaccount.yaml.tpl | 13 ++ go/dexop/helm/values.yaml | 106 +++++++++++++++ 13 files changed, 588 insertions(+) create mode 100644 go/dexop/helm/.helmignore create mode 100644 go/dexop/helm/Chart.yaml create mode 100644 go/dexop/helm/crds/clients.yaml create mode 100644 go/dexop/helm/templates/NOTES.txt create mode 100644 go/dexop/helm/templates/_helpers.tpl create mode 100644 go/dexop/helm/templates/clusterrole.yaml.tpl create mode 100644 go/dexop/helm/templates/clusterrolebinding.yaml.tpl create mode 100644 go/dexop/helm/templates/deployment.yaml.tpl create mode 100644 go/dexop/helm/templates/rolebinding.yaml.tpl create mode 100644 go/dexop/helm/templates/roles.yaml.tpl create mode 100644 go/dexop/helm/templates/service.yaml.tpl create mode 100644 go/dexop/helm/templates/serviceaccount.yaml.tpl create mode 100644 go/dexop/helm/values.yaml diff --git a/go/dexop/helm/.helmignore b/go/dexop/helm/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/go/dexop/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/go/dexop/helm/Chart.yaml b/go/dexop/helm/Chart.yaml new file mode 100644 index 000000000..db5bc8727 --- /dev/null +++ b/go/dexop/helm/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: dexop +description: Kubernetes operator to manage Dex Oauth2 clients. +type: application +version: 0.0.1 +appVersion: "0.0.1" diff --git a/go/dexop/helm/crds/clients.yaml b/go/dexop/helm/crds/clients.yaml new file mode 100644 index 000000000..304056802 --- /dev/null +++ b/go/dexop/helm/crds/clients.yaml @@ -0,0 +1,73 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: clients.dex.rax.io +spec: + group: dex.rax.io + names: + kind: Client + listKind: ClientList + plural: clients + singular: client + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Client is the Schema for the clients API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClientSpec defines the desired state of Client + properties: + generateSecret: + type: boolean + logoURL: + type: string + name: + type: string + public: + type: boolean + redirectURIs: + items: + type: string + type: array + secretName: + type: string + secretNamespace: + type: string + trustedPeers: + items: + type: string + type: array + required: + - name + - redirectURIs + type: object + status: + description: ClientStatus defines the observed state of Client + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/go/dexop/helm/templates/NOTES.txt b/go/dexop/helm/templates/NOTES.txt new file mode 100644 index 000000000..3ae05a178 --- /dev/null +++ b/go/dexop/helm/templates/NOTES.txt @@ -0,0 +1,11 @@ +dexop has been installed. + +Please make sure that you have created Secret '{{ .Values.dex.secret}}' in '{{ .Release.Namespace }} namespace so that it can connect to your Dex instance ({{ + .Values.dex.address }}). + + +The '{{ .Values.dex.secret }}' should be of `kubernetes.io/tls` type and MUST have following keys: + +- tls.crt +- tls.key +- ca.pem diff --git a/go/dexop/helm/templates/_helpers.tpl b/go/dexop/helm/templates/_helpers.tpl new file mode 100644 index 000000000..6da26db84 --- /dev/null +++ b/go/dexop/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "dexop.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "dexop.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "dexop.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "dexop.labels" -}} +helm.sh/chart: {{ include "dexop.chart" . }} +{{ include "dexop.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "dexop.selectorLabels" -}} +app.kubernetes.io/name: {{ include "dexop.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "dexop.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "dexop.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/go/dexop/helm/templates/clusterrole.yaml.tpl b/go/dexop/helm/templates/clusterrole.yaml.tpl new file mode 100644 index 000000000..6d064f16c --- /dev/null +++ b/go/dexop/helm/templates/clusterrole.yaml.tpl @@ -0,0 +1,124 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dexop-metrics-reader + labels: + {{- include "dexop.labels" . | nindent 4 }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dexop-metrics-auth-role + labels: + {{- include "dexop.labels" . | nindent 4 }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "dexop.labels" . | nindent 4 }} + name: dexop-client-editor-role +rules: +- apiGroups: + - dex.rax.io + resources: + - clients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dex.rax.io + resources: + - clients/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "dexop.labels" . | nindent 4 }} + name: dexop-client-viewer-role +rules: +- apiGroups: + - dex.rax.io + resources: + - clients + verbs: + - get + - list + - watch +- apiGroups: + - dex.rax.io + resources: + - clients/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dexop-manager-role + labels: + {{- include "dexop.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - dex.rax.io + resources: + - clients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dex.rax.io + resources: + - clients/finalizers + verbs: + - update +- apiGroups: + - dex.rax.io + resources: + - clients/status + verbs: + - get + - patch + - update diff --git a/go/dexop/helm/templates/clusterrolebinding.yaml.tpl b/go/dexop/helm/templates/clusterrolebinding.yaml.tpl new file mode 100644 index 000000000..1182ab6ce --- /dev/null +++ b/go/dexop/helm/templates/clusterrolebinding.yaml.tpl @@ -0,0 +1,30 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + {{- include "dexop.labels" . | nindent 4 }} + name: dexop-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dexop-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "dexop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dexop-metrics-auth-rolebinding + labels: + {{- include "dexop.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dexop-metrics-auth-role +subjects: +- kind: ServiceAccount + name: {{ include "dexop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/go/dexop/helm/templates/deployment.yaml.tpl b/go/dexop/helm/templates/deployment.yaml.tpl new file mode 100644 index 000000000..28578e16a --- /dev/null +++ b/go/dexop/helm/templates/deployment.yaml.tpl @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "dexop.fullname" . }}-controller-manager + labels: + control-plane: controller-manager + {{- include "dexop.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "dexop.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "dexop.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "dexop.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + terminationGracePeriodSeconds: 10 + containers: + - name: manager + args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + - --dex-ca-path=/run/secrets/dex/ca.pem + - --dex-cert-path=/run/secrets/dex/tls.crt + - --dex-key-path=/run/secrets/dex/tls.key + - --dex-host={{ .Values.dex.address }} + command: + - /manager + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - mountPath: /run/secrets/dex/ + name: client-secret + readOnly: true + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 12 }} + {{- end }} + - name: client-secret + secret: + secretName: {{ .Values.dex.secret }} + optional: false diff --git a/go/dexop/helm/templates/rolebinding.yaml.tpl b/go/dexop/helm/templates/rolebinding.yaml.tpl new file mode 100644 index 000000000..f38a9c150 --- /dev/null +++ b/go/dexop/helm/templates/rolebinding.yaml.tpl @@ -0,0 +1,16 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + {{- include "dexop.labels" . | nindent 4 }} + name: dexop-leader-election-rolebinding + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: dexop-leader-election-role +subjects: +- kind: ServiceAccount + name: {{ include "dexop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/go/dexop/helm/templates/roles.yaml.tpl b/go/dexop/helm/templates/roles.yaml.tpl new file mode 100644 index 000000000..d9b898057 --- /dev/null +++ b/go/dexop/helm/templates/roles.yaml.tpl @@ -0,0 +1,40 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "dexop.labels" . | nindent 4 }} + name: dexop-leader-election-role + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/go/dexop/helm/templates/service.yaml.tpl b/go/dexop/helm/templates/service.yaml.tpl new file mode 100644 index 000000000..7bf242497 --- /dev/null +++ b/go/dexop/helm/templates/service.yaml.tpl @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "dexop.fullname" . }}-manager-metrics + labels: + {{- include "dexop.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: 8443 + targetPort: 8443 + protocol: TCP + name: https + selector: + {{- include "dexop.selectorLabels" . | nindent 4 }} diff --git a/go/dexop/helm/templates/serviceaccount.yaml.tpl b/go/dexop/helm/templates/serviceaccount.yaml.tpl new file mode 100644 index 000000000..26a9e3fc8 --- /dev/null +++ b/go/dexop/helm/templates/serviceaccount.yaml.tpl @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "dexop.serviceAccountName" . }} + labels: + {{- include "dexop.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/go/dexop/helm/values.yaml b/go/dexop/helm/values.yaml new file mode 100644 index 000000000..db78eee2f --- /dev/null +++ b/go/dexop/helm/values.yaml @@ -0,0 +1,106 @@ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: ghcr.io/rackerlabs/understack/dexop + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +dex: + address: dex-api.dex.cluster.local:5557 + secret: dexop-dex-client From 1a45cc60b6c96465060237c57f549fac7f4b246f Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 14:40:34 +0100 Subject: [PATCH 26/37] dexop: refactor deletion --- .../internal/controller/client_controller.go | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index e42b67bdd..436e0ba76 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -66,21 +66,10 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr reqLogger.Info("Reconciling Client") - // delete if no longer needed - deleteRequested := clientSpec.GetDeletionTimestamp() != nil - if deleteRequested { - if controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { - if err := r.finalizeDeletion(reqLogger, clientSpec, r.DexManager); err != nil { - return ctrl.Result{}, err - } - - // remove finalizer - controllerutil.RemoveFinalizer(clientSpec, dexFinalizer) - err := r.Update(ctx, clientSpec) - if err != nil { - return ctrl.Result{}, err - } - } + deleteRequested, err := r.handleDeletion(ctx, clientSpec) + if err != nil { + return ctrl.Result{}, err + } else if deleteRequested { return ctrl.Result{}, nil } @@ -160,16 +149,6 @@ func (r *ClientReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -// finalizeDeletion removes an OAuth2 client from Dex when the corresponding Client resource is deleted. -// It uses the DexManager to send a request to Dex over gRPC to delete the client. -func (r *ClientReconciler) finalizeDeletion(reqLogger logr.Logger, c *dexv1alpha1.Client, mgr *dexmgr.DexManager) error { - reqLogger.Info("Client is being removed") - if _, err := mgr.RemoveOauth2Client(c.Spec.Name); err != nil { - return err - } - return nil -} - func (r *ClientReconciler) getClientSpec(ctx context.Context, namespacedName types.NamespacedName) (*dexv1alpha1.Client, error) { clientSpec := &dexv1alpha1.Client{} if err := r.Get(ctx, namespacedName, clientSpec); err != nil { @@ -180,3 +159,26 @@ func (r *ClientReconciler) getClientSpec(ctx context.Context, namespacedName typ } return clientSpec, nil } + +// handleDeletion() removes an OAuth2 client from Dex when the corresponding Client resource is deleted. +// It uses the DexManager to send a request to Dex over gRPC to delete the client. +func (r *ClientReconciler) handleDeletion(ctx context.Context, clientSpec *dexv1alpha1.Client) (bool, error) { + // delete if no longer needed + deleteRequested := clientSpec.GetDeletionTimestamp() != nil + if deleteRequested { + if controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { + if _, err := r.DexManager.RemoveOauth2Client(clientSpec.Spec.Name); err != nil { + return deleteRequested, err + } + + // remove finalizer + controllerutil.RemoveFinalizer(clientSpec, dexFinalizer) + err := r.Update(ctx, clientSpec) + if err != nil { + return deleteRequested, err + } + } + return deleteRequested, nil + } + return deleteRequested, nil +} From a312acf09b8984e2398d77c3df372299d1b51427 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 15:01:35 +0100 Subject: [PATCH 27/37] dexop: refactor adding finalizer --- .../internal/controller/client_controller.go | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 436e0ba76..7d647829a 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -73,13 +73,8 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, nil } - // add finalizer - if !controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { - controllerutil.AddFinalizer(clientSpec, dexFinalizer) - err := r.Update(ctx, clientSpec) - if err != nil { - return ctrl.Result{}, err - } + if err = r.addFinalizer(ctx, clientSpec); err != nil { + return ctrl.Result{}, err } // if secretName specified, read the secret @@ -182,3 +177,16 @@ func (r *ClientReconciler) handleDeletion(ctx context.Context, clientSpec *dexv1 } return deleteRequested, nil } + +// adds finalizer on the Resource so that deletion can be handled gracefully +func (r *ClientReconciler) addFinalizer(ctx context.Context, clientSpec *dexv1alpha1.Client) error { + // add finalizer + if !controllerutil.ContainsFinalizer(clientSpec, dexFinalizer) { + controllerutil.AddFinalizer(clientSpec, dexFinalizer) + err := r.Update(ctx, clientSpec) + if err != nil { + return err + } + } + return nil +} From 6429441fe35fb14c191593b4d8799c764c45f425 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 15:25:29 +0100 Subject: [PATCH 28/37] dexop: refactor secret handling --- .../internal/controller/client_controller.go | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 7d647829a..d0cf2ea36 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -77,39 +77,8 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // if secretName specified, read the secret - secretmgr := new(SecretManager) - if clientSpec.Spec.SecretName != "" { - if clientSpec.Spec.SecretNamespace == "" { - clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace - } - // read existing or generate a secret - value, err := secretmgr.readSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) - if err != nil { - if errors.IsNotFound(err) && clientSpec.Spec.GenerateSecret { - secret, err := secretmgr.generateSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) - if err != nil { - reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) - return ctrl.Result{}, err - } - - if secret.Data["secret"] == nil { - reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) - } - value = string(secret.Data["secret"]) - if err = ctrl.SetControllerReference(clientSpec, secret, r.Scheme); err != nil { - return ctrl.Result{}, err - } - - if err = r.Update(ctx, secret); err != nil { - return ctrl.Result{}, err - } - } else { - reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) - return ctrl.Result{}, err - } - } - clientSpec.Spec.SecretValue = value + if err = r.manageSecret(ctx, req, clientSpec, reqLogger); err != nil { + return ctrl.Result{}, err } existing, err := r.DexManager.GetOauth2Client(clientSpec.Spec.Name) @@ -190,3 +159,42 @@ func (r *ClientReconciler) addFinalizer(ctx context.Context, clientSpec *dexv1al } return nil } + +// Handle reading/generating Kubernetes secret to setup the password that will be used by Oauth2 client +func (r *ClientReconciler) manageSecret(ctx context.Context, req ctrl.Request, clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) error { + // if secretName specified, read the secret + secretmgr := new(SecretManager) + if clientSpec.Spec.SecretName != "" { + if clientSpec.Spec.SecretNamespace == "" { + clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace + } + // read existing or generate a secret + value, err := secretmgr.readSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + if err != nil { + if errors.IsNotFound(err) && clientSpec.Spec.GenerateSecret { + secret, err := secretmgr.generateSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + if err != nil { + reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) + return err + } + + if secret.Data["secret"] == nil { + reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) + } + value = string(secret.Data["secret"]) + if err = ctrl.SetControllerReference(clientSpec, secret, r.Scheme); err != nil { + return err + } + + if err = r.Update(ctx, secret); err != nil { + return err + } + } else { + reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) + return err + } + } + clientSpec.Spec.SecretValue = value + } + return nil +} From e245b5b77adfe5dfce930f8b8e41378ffc336423 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 16:15:29 +0100 Subject: [PATCH 29/37] dexop: refactor adding/updating client --- .../internal/controller/client_controller.go | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index d0cf2ea36..f9521fc20 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -63,7 +63,6 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if clientSpec == nil { return ctrl.Result{}, nil } - reqLogger.Info("Reconciling Client") deleteRequested, err := r.handleDeletion(ctx, clientSpec) @@ -81,25 +80,15 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - existing, err := r.DexManager.GetOauth2Client(clientSpec.Spec.Name) + already_exists, err := r.createClient(clientSpec, reqLogger) if err != nil { - if strings.Contains(strings.ToLower(err.Error()), "not found") { - ctrl.Log.Info("Client does not exist in Dex. Creating one.", "name", clientSpec.Spec.Name) - if _, err = r.DexManager.CreateOauth2Client(clientSpec); err != nil { - reqLogger.Error(err, "Unable to create client in dex") - return ctrl.Result{}, err - } - return ctrl.Result{}, nil - } return ctrl.Result{}, err } // update - if existing != nil { - reqLogger.Info("making an UpdateOauth2Client call") - err = r.DexManager.UpdateOauth2Client(clientSpec) - if err != nil { - reqLogger.Error(err, "after UpdateOauth2Client") + if already_exists { + if err := r.DexManager.UpdateOauth2Client(clientSpec); err != nil { + reqLogger.Error(err, "failed on UpdateOauth2Client") return ctrl.Result{}, err } } @@ -192,9 +181,19 @@ func (r *ClientReconciler) manageSecret(ctx context.Context, req ctrl.Request, c } else { reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) return err +func (r *ClientReconciler) createClient(clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) (bool, error) { + _, err := r.DexManager.GetOauth2Client(clientSpec.Spec.Name) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + ctrl.Log.Info("Client does not exist in Dex. Creating one.", "name", clientSpec.Spec.Name) + if _, err = r.DexManager.CreateOauth2Client(clientSpec); err != nil { + reqLogger.Error(err, "Unable to create client in dex") + return false, err } + return true, nil } - clientSpec.Spec.SecretValue = value + // other errors + return false, err } - return nil + return true, nil } From 95b9ed88238f87cead09e6b284cf8c6d1436910e Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 16:19:37 +0100 Subject: [PATCH 30/37] dexop: refactor secret handling further --- .../internal/controller/client_controller.go | 87 +++++++++++++------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index f9521fc20..e470de7a6 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -151,36 +151,67 @@ func (r *ClientReconciler) addFinalizer(ctx context.Context, clientSpec *dexv1al // Handle reading/generating Kubernetes secret to setup the password that will be used by Oauth2 client func (r *ClientReconciler) manageSecret(ctx context.Context, req ctrl.Request, clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) error { - // if secretName specified, read the secret secretmgr := new(SecretManager) - if clientSpec.Spec.SecretName != "" { - if clientSpec.Spec.SecretNamespace == "" { - clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace + if clientSpec.Spec.SecretName == "" { + return nil + } + + if clientSpec.Spec.SecretNamespace == "" { + clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace + } + + secretValue, err := r.readOrGenerateSecret(ctx, secretmgr, clientSpec, reqLogger) + if err != nil { + return err + } + + clientSpec.Spec.SecretValue = secretValue + return nil +} + +// reads the password from an existing Secret, fallback to generating new one +func (r *ClientReconciler) readOrGenerateSecret(ctx context.Context, secretmgr *SecretManager, clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) (string, error) { + secret, err := secretmgr.readSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + if err != nil { + if errors.IsNotFound(err) && clientSpec.Spec.GenerateSecret { + return r.generateSecret(ctx, secretmgr, clientSpec, reqLogger) } - // read existing or generate a secret - value, err := secretmgr.readSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) - if err != nil { - if errors.IsNotFound(err) && clientSpec.Spec.GenerateSecret { - secret, err := secretmgr.generateSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) - if err != nil { - reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) - return err - } - - if secret.Data["secret"] == nil { - reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) - } - value = string(secret.Data["secret"]) - if err = ctrl.SetControllerReference(clientSpec, secret, r.Scheme); err != nil { - return err - } - - if err = r.Update(ctx, secret); err != nil { - return err - } - } else { - reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) - return err + reqLogger.Error(err, "Unable to read secret", "secretName", clientSpec.Spec.SecretName) + return "", err + } + + if secret == "" { + reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) + return "", nil + } + + return secret, nil +} + +// generateSecret creates a Kubernetes secret with randomly generated password. +func (r *ClientReconciler) generateSecret(ctx context.Context, secretmgr *SecretManager, clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) (string, error) { + secret, err := secretmgr.generateSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + if err != nil { + reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) + return "", err + } + + if err = ctrl.SetControllerReference(clientSpec, secret, r.Scheme); err != nil { + return "", err + } + + if err = r.Update(ctx, secret); err != nil { + return "", err + } + + if secret.Data["secret"] == nil { + reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) + return "", nil + } + + return string(secret.Data["secret"]), nil +} + func (r *ClientReconciler) createClient(clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) (bool, error) { _, err := r.DexManager.GetOauth2Client(clientSpec.Spec.Name) if err != nil { From ddf8f53e26850d287f73524bb278ed7d8f8d9623 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 28 Apr 2025 16:47:44 +0100 Subject: [PATCH 31/37] dexop: don't build images on PR --- .github/workflows/build-dexop.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build-dexop.yaml b/.github/workflows/build-dexop.yaml index ab8f5edfa..8efa24f3e 100644 --- a/.github/workflows/build-dexop.yaml +++ b/.github/workflows/build-dexop.yaml @@ -3,16 +3,11 @@ name: build-dexop-images on: workflow_dispatch: - pull_request: - paths: - - "go/dexop/**" push: tags: - dexop-v* paths: - "go/dexop/**" - merge_group: - types: [checks_requested] jobs: build-ghcr-registry: From 9b699c236887872970d73d9ead42860ac0dd5c55 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 29 Apr 2025 07:51:41 +0100 Subject: [PATCH 32/37] dexop: migrate to new golangci --- go/dexop/.golangci.yml | 53 +++++++++++-------- .../internal/controller/client_controller.go | 2 +- go/dexop/test/utils/utils.go | 2 +- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/go/dexop/.golangci.yml b/go/dexop/.golangci.yml index ca69a11f6..3efad3901 100644 --- a/go/dexop/.golangci.yml +++ b/go/dexop/.golangci.yml @@ -1,32 +1,13 @@ +version: "2" run: - timeout: 5m allow-parallel-runners: true - -issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) - exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" - linters: - - dupl - - lll linters: - disable-all: true + default: none enable: - dupl - errcheck - - exportloopref - goconst - gocyclo - - gofmt - - goimports - - gosimple - govet - ineffassign - lll @@ -34,7 +15,35 @@ linters: - nakedret - prealloc - staticcheck - - typecheck - unconvert - unparam - unused + exclusions: + generated: lax + rules: + - linters: + - lll + path: api/* + - linters: + - dupl + - lll + path: internal/* + paths: + - third_party$ + - builtin$ + - examples$ + settings: + staticcheck: + dot-import-whitelist: + - fmt + - github.com/onsi/ginkgo/v2 +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index e470de7a6..68aeeb859 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -157,7 +157,7 @@ func (r *ClientReconciler) manageSecret(ctx context.Context, req ctrl.Request, c } if clientSpec.Spec.SecretNamespace == "" { - clientSpec.Spec.SecretNamespace = req.NamespacedName.Namespace + clientSpec.Spec.SecretNamespace = req.Namespace } secretValue, err := r.readOrGenerateSecret(ctx, secretmgr, clientSpec, reqLogger) diff --git a/go/dexop/test/utils/utils.go b/go/dexop/test/utils/utils.go index da8f13c56..6b8df6d1b 100644 --- a/go/dexop/test/utils/utils.go +++ b/go/dexop/test/utils/utils.go @@ -135,6 +135,6 @@ func GetProjectDir() (string, error) { if err != nil { return wd, err } - wd = strings.Replace(wd, "/test/e2e", "", -1) + wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } From fd741d78dfbae39aadb360ed179b4bc96895823a Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 29 Apr 2025 10:55:19 +0100 Subject: [PATCH 33/37] dexop: make Secrets compatible with Understack This changes how the automatically generated Kubernetes Secret looks like: - the `secret` key was renamed to `client-secret` - the `client-id` key is now populated - the `issuer` is populated Caveat: The Issuer information can be obtained from Dex dynamically only starting from newest version (2.42) of Dex. To avoid triggering failed discovery, the same information can be provided as a command line argument. --- go/dexop/api/v1alpha1/client_types.go | 1 + go/dexop/cmd/main.go | 6 ++- go/dexop/dex/client.go | 20 ++++++++-- go/dexop/helm/templates/deployment.yaml.tpl | 3 ++ go/dexop/helm/values.yaml | 3 ++ .../internal/controller/client_controller.go | 8 +++- .../controller/client_controller_test.go | 40 +++++++++++++++++-- .../internal/controller/secret_manager.go | 17 ++++---- 8 files changed, 80 insertions(+), 18 deletions(-) diff --git a/go/dexop/api/v1alpha1/client_types.go b/go/dexop/api/v1alpha1/client_types.go index a1993047a..5706be63c 100644 --- a/go/dexop/api/v1alpha1/client_types.go +++ b/go/dexop/api/v1alpha1/client_types.go @@ -29,6 +29,7 @@ type ClientSpec struct { SecretName string `json:"secretName,omitempty"` SecretNamespace string `json:"secretNamespace,omitempty"` SecretValue string `json:"-"` + Issuer string `json:"-"` GenerateSecret bool `json:"generateSecret,omitempty"` RedirectURIs []string `json:"redirectURIs"` LogoUrl string `json:"logoURL,omitempty"` diff --git a/go/dexop/cmd/main.go b/go/dexop/cmd/main.go index 7c825773d..da6a25375 100644 --- a/go/dexop/cmd/main.go +++ b/go/dexop/cmd/main.go @@ -79,6 +79,7 @@ func main() { "Path to the client certificate for Dex API") flag.StringVar(&dexConf.ClientKeyPath, "dex-key-path", "./grpc_client.key", "Path to the client key for Dex API") + flag.StringVar(&dexConf.Issuer, "dex-issuer", "", "force dex Issuer value to be inserted into created secrets.") opts := zap.Options{ Development: true, } @@ -191,10 +192,11 @@ type DexConfig struct { CAPath string ClientKeyPath string ClientCertPath string + Issuer string } -func newDexManager(config *DexConfig) (*dexmgr.DexManager, error) { - mgr, err := dexmgr.NewDexManager(config.Address, config.CAPath, config.ClientKeyPath, config.ClientCertPath) +func newDexManager(cfg *DexConfig) (*dexmgr.DexManager, error) { + mgr, err := dexmgr.NewDexManager(cfg.Address, cfg.CAPath, cfg.ClientKeyPath, cfg.ClientCertPath, cfg.Issuer) if err != nil { setupLog.Error(err, "While getting the DexManager") } diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index 43e8c4183..9aff9a7e1 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -16,6 +16,7 @@ import ( type DexManager struct { Client dexapi.DexClient + Issuer string } // Creates new Oauth2 client in Dex @@ -86,6 +87,19 @@ func (d *DexManager) UpdateOauth2Client(clientSpec *dexv1alpha1.Client) error { return err } +func (d *DexManager) GetIssuer() (string, error) { + // fallback for dex versions that don't support Discovery over GRPC + if d.Issuer != "" { + return d.Issuer, nil + } + + result, err := d.Client.GetDiscovery(context.TODO(), &dexapi.DiscoveryReq{}) + if err != nil { + return "", err + } + return result.GetIssuer(), nil +} + func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexClient, error) { cPool := x509.NewCertPool() caCert, err := os.ReadFile(caPath) @@ -120,13 +134,13 @@ func NewInsecureTestManager(grpcAddr string) (*DexManager, error) { if err != nil { return nil, err } - return &DexManager{Client: dexapi.NewDexClient(grpcConn)}, nil + return &DexManager{Client: dexapi.NewDexClient(grpcConn), Issuer: "http://127.0.0.1:15556/dex"}, nil } -func NewDexManager(host, caCert, clientKey, clientCert string) (*DexManager, error) { +func NewDexManager(host, caCert, clientKey, clientCert, issuer string) (*DexManager, error) { client, err := newDexClient(host, caCert, clientKey, clientCert) if err != nil { return nil, fmt.Errorf("failed creating dex client: %w", err) } - return &DexManager{Client: client}, nil + return &DexManager{Client: client, Issuer: issuer}, nil } diff --git a/go/dexop/helm/templates/deployment.yaml.tpl b/go/dexop/helm/templates/deployment.yaml.tpl index 28578e16a..674034f90 100644 --- a/go/dexop/helm/templates/deployment.yaml.tpl +++ b/go/dexop/helm/templates/deployment.yaml.tpl @@ -40,6 +40,9 @@ spec: - --dex-cert-path=/run/secrets/dex/tls.crt - --dex-key-path=/run/secrets/dex/tls.key - --dex-host={{ .Values.dex.address }} + {{ with .Values.dex.issuer }} + - --dex-issuer={{ . }} + {{- end }} command: - /manager securityContext: diff --git a/go/dexop/helm/values.yaml b/go/dexop/helm/values.yaml index db78eee2f..83105f6de 100644 --- a/go/dexop/helm/values.yaml +++ b/go/dexop/helm/values.yaml @@ -104,3 +104,6 @@ volumeMounts: [] dex: address: dex-api.dex.cluster.local:5557 secret: dexop-dex-client + # 'issuer' must be set explicitly if using dex version older than 2.42 + # + # issuer: http://dex.instance.local/dex diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 68aeeb859..8c96e36dd 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -110,6 +110,12 @@ func (r *ClientReconciler) getClientSpec(ctx context.Context, namespacedName typ } return nil, err } + // populate issuer + issuer, err := r.DexManager.GetIssuer() + if err != nil { + return nil, err + } + clientSpec.Spec.Issuer = issuer return clientSpec, nil } @@ -190,7 +196,7 @@ func (r *ClientReconciler) readOrGenerateSecret(ctx context.Context, secretmgr * // generateSecret creates a Kubernetes secret with randomly generated password. func (r *ClientReconciler) generateSecret(ctx context.Context, secretmgr *SecretManager, clientSpec *dexv1alpha1.Client, reqLogger logr.Logger) (string, error) { - secret, err := secretmgr.generateSecret(r, ctx, clientSpec.Spec.SecretName, clientSpec.Spec.SecretNamespace) + secret, err := secretmgr.generateSecret(r, ctx, clientSpec) if err != nil { reqLogger.Error(err, "Unable to write secret", "secretName", clientSpec.Spec.SecretName) return "", err diff --git a/go/dexop/internal/controller/client_controller_test.go b/go/dexop/internal/controller/client_controller_test.go index 931362dba..ade29f31d 100644 --- a/go/dexop/internal/controller/client_controller_test.go +++ b/go/dexop/internal/controller/client_controller_test.go @@ -110,7 +110,7 @@ var _ = Describe("Client Controller", func() { secretObj := &v1.Secret{} err = k8sClient.Get(ctx, types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: secretName}, secretObj) - Expect(len(secretObj.Data["secret"])).To(Equal(48)) + Expect(len(secretObj.Data["client-secret"])).To(Equal(48)) Expect(err).NotTo(HaveOccurred()) }) It("should successfully reconcile the resource", func() { @@ -140,7 +140,7 @@ var _ = Describe("Client Controller", func() { Expect(err).NotTo(HaveOccurred()) secretObj := &v1.Secret{} Expect(k8sClient.Get(ctx, typesNamespacedSecretName, secretObj)).To(Succeed()) - secretObj.Data["secret"] = []byte("newSecret") + secretObj.Data["client-secret"] = []byte("newSecret") By("reconcile after changing the secret") Expect(k8sClient.Update(ctx, secretObj)).To(Succeed()) @@ -208,7 +208,39 @@ var _ = Describe("Client Controller", func() { By("checking if the Secret has been recreated") newSecret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace}} - Expect(k8sClient.Get(ctx, typesNamespacedSecretName, newSecret)) + Expect(k8sClient.Get(ctx, typesNamespacedSecretName, newSecret)).To(Succeed()) + }) + + It("populates the issuer in Secret", func() { + By("reconciling the first time") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace}} + Expect(k8sClient.Get(ctx, typesNamespacedSecretName, secret)).To(Succeed()) + Expect(string(secret.Data["issuer"])).To(Equal("http://127.0.0.1:15556/dex")) + }) + + It("populates the client-id in Secret", func() { + By("reconciling the first time") + controllerReconciler := &ClientReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + DexManager: dex, + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: typesNamespacedSecretName.Name, Namespace: typesNamespacedSecretName.Namespace}} + Expect(k8sClient.Get(ctx, typesNamespacedSecretName, secret)).To(Succeed()) + Expect(string(secret.Data["client-id"])).To(Equal("fred-client")) }) }) @@ -239,7 +271,7 @@ var _ = Describe("Client Controller", func() { By("creating secret") secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: "default"}, - Data: map[string][]byte{"secret": []byte("abc")}, + Data: map[string][]byte{"client-secret": []byte("abc")}, } Expect(k8sClient.Create(ctx, secret)).To(Succeed()) diff --git a/go/dexop/internal/controller/secret_manager.go b/go/dexop/internal/controller/secret_manager.go index 8f26b35fc..32b0d7b4a 100644 --- a/go/dexop/internal/controller/secret_manager.go +++ b/go/dexop/internal/controller/secret_manager.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + dexv1alpha1 "github.com/rackerlabs/understack/go/dexop/api/v1alpha1" "github.com/sethvargo/go-password/password" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,20 +22,20 @@ func (s SecretManager) readSecret(r *ClientReconciler, ctx context.Context, name return "", err } - if value, ok := secret.Data["secret"]; ok { + if value, ok := secret.Data["client-secret"]; ok { return string(value), nil } - return "", fmt.Errorf("secret key not found") + return "", fmt.Errorf("client-secret key not found") } -func (s SecretManager) writeSecret(r *ClientReconciler, ctx context.Context, name, namespace, value string) (*v1.Secret, error) { +func (s SecretManager) writeSecret(r *ClientReconciler, ctx context.Context, clientSpec *dexv1alpha1.Client, value string) (*v1.Secret, error) { secret := &v1.Secret{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, + Name: clientSpec.Spec.SecretName, + Namespace: clientSpec.Spec.SecretNamespace, }, - Data: map[string][]byte{"secret": []byte(value)}, + Data: map[string][]byte{"client-secret": []byte(value), "issuer": []byte(clientSpec.Spec.Issuer), "client-id": []byte(clientSpec.Spec.Name)}, Type: "Opaque", } @@ -45,10 +46,10 @@ func (s SecretManager) writeSecret(r *ClientReconciler, ctx context.Context, nam return secret, nil } -func (s SecretManager) generateSecret(r *ClientReconciler, ctx context.Context, name, namespace string) (*v1.Secret, error) { +func (s SecretManager) generateSecret(r *ClientReconciler, ctx context.Context, clientSpec *dexv1alpha1.Client) (*v1.Secret, error) { res, err := password.Generate(48, 10, 10, false, false) if err != nil { return nil, err } - return s.writeSecret(r, ctx, name, namespace, res) + return s.writeSecret(r, ctx, clientSpec, res) } From 2ea0d158924d1b176280d61111ad08876ddaffc5 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 29 Apr 2025 14:38:02 +0100 Subject: [PATCH 34/37] dexop: remove unnecessary TypeMeta --- go/dexop/internal/controller/secret_manager.go | 1 - 1 file changed, 1 deletion(-) diff --git a/go/dexop/internal/controller/secret_manager.go b/go/dexop/internal/controller/secret_manager.go index 32b0d7b4a..b948c7551 100644 --- a/go/dexop/internal/controller/secret_manager.go +++ b/go/dexop/internal/controller/secret_manager.go @@ -30,7 +30,6 @@ func (s SecretManager) readSecret(r *ClientReconciler, ctx context.Context, name func (s SecretManager) writeSecret(r *ClientReconciler, ctx context.Context, clientSpec *dexv1alpha1.Client, value string) (*v1.Secret, error) { secret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: clientSpec.Spec.SecretName, Namespace: clientSpec.Spec.SecretNamespace, From 44f966fd07304b4defed4b9dba09c2f63a7110db Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 7 May 2025 14:01:44 +0100 Subject: [PATCH 35/37] dexop: better error handling for certs --- go/dexop/dex/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index 9aff9a7e1..06172ed8c 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -104,7 +104,7 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC cPool := x509.NewCertPool() caCert, err := os.ReadFile(caPath) if err != nil { - return nil, fmt.Errorf("invalid CA crt file: %s", caPath) + return nil, fmt.Errorf("invalid CA crt file (%s): %w", caPath, err) } if !cPool.AppendCertsFromPEM(caCert) { return nil, fmt.Errorf("failed to parse CA crt") @@ -112,7 +112,7 @@ func newDexClient(hostAndPort, caPath, clientKey, clientCrt string) (dexapi.DexC clientCert, err := tls.LoadX509KeyPair(clientCrt, clientKey) if err != nil { - return nil, fmt.Errorf("invalid client crt file: %s", clientCrt) + return nil, fmt.Errorf("invalid client crt(%s) or key(%s) file: %w", clientCrt, clientKey, err) } clientTLSConfig := &tls.Config{ From 1357548f9651afa8dc25832a46ee561face5ef34 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 7 May 2025 14:13:53 +0100 Subject: [PATCH 36/37] dexop: style --- go/dexop/internal/controller/client_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 8c96e36dd..4c7b2be13 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -68,7 +68,8 @@ func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr deleteRequested, err := r.handleDeletion(ctx, clientSpec) if err != nil { return ctrl.Result{}, err - } else if deleteRequested { + } + if deleteRequested { return ctrl.Result{}, nil } From d237a170c93fed0c16d31810a063f3a1c9e4db13 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 7 May 2025 14:47:18 +0100 Subject: [PATCH 37/37] dexop: add missing nil checks --- go/dexop/dex/client.go | 3 +++ go/dexop/internal/controller/client_controller.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go/dexop/dex/client.go b/go/dexop/dex/client.go index 06172ed8c..c31c97f72 100644 --- a/go/dexop/dex/client.go +++ b/go/dexop/dex/client.go @@ -21,6 +21,9 @@ type DexManager struct { // Creates new Oauth2 client in Dex func (d *DexManager) CreateOauth2Client(clientSpec *dexv1alpha1.Client) (*dexapi.CreateClientResp, error) { + if clientSpec == nil { + return nil, fmt.Errorf("clientSpec is missing") + } request := &dexapi.CreateClientReq{ Client: &dexapi.Client{ Id: clientSpec.Spec.Name, diff --git a/go/dexop/internal/controller/client_controller.go b/go/dexop/internal/controller/client_controller.go index 4c7b2be13..66fae26e5 100644 --- a/go/dexop/internal/controller/client_controller.go +++ b/go/dexop/internal/controller/client_controller.go @@ -211,7 +211,7 @@ func (r *ClientReconciler) generateSecret(ctx context.Context, secretmgr *Secret return "", err } - if secret.Data["secret"] == nil { + if secret.Data == nil || secret.Data["secret"] == nil { reqLogger.Error(nil, "Secret data is missing", "SecretName", clientSpec.Spec.SecretName) return "", nil }