From 25c940cfd3fc05f352e113d18b1950e3c47805dd Mon Sep 17 00:00:00 2001 From: HaimKortovich Date: Wed, 25 Mar 2026 16:44:42 -0500 Subject: [PATCH] Init commit --- .envrc | 1 + .gitignore | 49 + build/flake.lock | 73 + build/flake.nix | 52 + build/push-image.sh | 63 + src/.dockerignore | 4 + src/.gitignore | 26 + src/Dockerfile | 33 + src/Makefile | 291 ++++ src/PROJECT | 87 ++ src/README.md | 94 ++ src/api/v1alpha1/action_types.go | 100 ++ src/api/v1alpha1/apiapp_types.go | 122 ++ src/api/v1alpha1/condition_types.go | 32 + src/api/v1alpha1/connection_types.go | 122 ++ src/api/v1alpha1/flow_types.go | 97 ++ src/api/v1alpha1/groupversion_info.go | 36 + src/api/v1alpha1/machineuser_types.go | 156 ++ src/api/v1alpha1/oidcapp_types.go | 149 ++ src/api/v1alpha1/organization_types.go | 92 ++ src/api/v1alpha1/project_types.go | 117 ++ src/api/v1alpha1/ref_types.go | 35 + src/api/v1alpha1/refresolver.go | 143 ++ src/api/v1alpha1/zz_generated.deepcopy.go | 1262 +++++++++++++++++ src/cmd/main.go | 150 ++ .../crd/bases/zitadel.github.com_actions.yaml | 173 +++ .../crd/bases/zitadel.github.com_apiapps.yaml | 176 +++ .../bases/zitadel.github.com_connections.yaml | 239 ++++ .../crd/bases/zitadel.github.com_flows.yaml | 227 +++ .../zitadel.github.com_machineusers.yaml | 278 ++++ .../bases/zitadel.github.com_oidcapps.yaml | 242 ++++ .../zitadel.github.com_organizations.yaml | 163 +++ .../bases/zitadel.github.com_projects.yaml | 239 ++++ src/config/crd/kustomization.yaml | 43 + src/config/crd/kustomizeconfig.yaml | 19 + src/config/default/kustomization.yaml | 144 ++ .../default/manager_auth_proxy_patch.yaml | 55 + src/config/default/manager_config_patch.yaml | 10 + src/config/manager/kustomization.yaml | 2 + src/config/manager/manager.yaml | 102 ++ src/config/rbac/action_editor_role.yaml | 31 + src/config/rbac/action_viewer_role.yaml | 27 + src/config/rbac/apiapp_editor_role.yaml | 31 + src/config/rbac/apiapp_viewer_role.yaml | 27 + .../rbac/auth_proxy_client_clusterrole.yaml | 16 + src/config/rbac/auth_proxy_role.yaml | 24 + src/config/rbac/auth_proxy_role_binding.yaml | 19 + src/config/rbac/auth_proxy_service.yaml | 21 + src/config/rbac/cluster_editor_role.yaml | 31 + src/config/rbac/connection_editor_role.yaml | 27 + src/config/rbac/connection_viewer_role.yaml | 23 + src/config/rbac/flow_editor_role.yaml | 31 + src/config/rbac/flow_viewer_role.yaml | 27 + src/config/rbac/kustomization.yaml | 25 + src/config/rbac/leader_election_role.yaml | 44 + .../rbac/leader_election_role_binding.yaml | 19 + src/config/rbac/machineuser_editor_role.yaml | 31 + src/config/rbac/machineuser_viewer_role.yaml | 27 + src/config/rbac/oidcapp_editor_role.yaml | 31 + src/config/rbac/oidcapp_viewer_role.yaml | 27 + src/config/rbac/organization_editor_role.yaml | 31 + src/config/rbac/organization_viewer_role.yaml | 27 + src/config/rbac/project_editor_role.yaml | 31 + src/config/rbac/project_viewer_role.yaml | 27 + src/config/rbac/role.yaml | 32 + src/config/rbac/role_binding.yaml | 19 + src/config/rbac/service_account.yaml | 12 + src/config/samples/kustomization.yaml | 4 + .../samples/zitadel_v1alpha1_connection.yaml | 9 + src/go.mod | 101 ++ src/go.sum | 265 ++++ src/hack/boilerplate.go.txt | 15 + .../controller/action_controller.gold | 192 +++ .../action_controller_finalizer.gold | 94 ++ .../controller/apiapp_controller.gold | 282 ++++ .../apiapp_controller_finalizer.gold | 91 ++ .../controller/connection_controller.go | 116 ++ .../connection_controller_finalizer.go | 69 + src/internal/controller/flow_controller.gold | 168 +++ .../controller/flow_controller_finalizer.gold | 82 ++ .../controller/instance_controller_test.gold | 84 ++ .../controller/machineuser_controller.gold | 417 ++++++ .../machineuser_controller_finalizer.gold | 78 + .../controller/oidcapp_controller.gold | 263 ++++ .../oidcapp_controller_finalizer.gold | 88 ++ .../controller/organization_controller.gold | 167 +++ .../organization_controller_finalizer.gold | 77 + .../controller/project_controller.gold | 314 ++++ .../project_controller_finalizer.gold | 76 + src/internal/controller/suite_test.go | 80 ++ src/pkg/builder/builder.go | 15 + src/pkg/builder/labels/labels.go | 21 + src/pkg/builder/metadata/metadata.go | 39 + src/pkg/builder/secret_builder.go | 35 + src/pkg/condition/condition.go | 68 + src/pkg/condition/pat.go | 25 + src/pkg/condition/ready.go | 70 + src/pkg/controller/core/controller.go | 111 ++ src/pkg/controller/core/finalizer.go | 73 + src/pkg/controller/core/types.go | 39 + src/pkg/zitadel/zitadel.go | 63 + 101 files changed, 9907 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 build/flake.lock create mode 100644 build/flake.nix create mode 100755 build/push-image.sh create mode 100644 src/.dockerignore create mode 100644 src/.gitignore create mode 100644 src/Dockerfile create mode 100644 src/Makefile create mode 100644 src/PROJECT create mode 100644 src/README.md create mode 100644 src/api/v1alpha1/action_types.go create mode 100644 src/api/v1alpha1/apiapp_types.go create mode 100644 src/api/v1alpha1/condition_types.go create mode 100644 src/api/v1alpha1/connection_types.go create mode 100644 src/api/v1alpha1/flow_types.go create mode 100644 src/api/v1alpha1/groupversion_info.go create mode 100644 src/api/v1alpha1/machineuser_types.go create mode 100644 src/api/v1alpha1/oidcapp_types.go create mode 100644 src/api/v1alpha1/organization_types.go create mode 100644 src/api/v1alpha1/project_types.go create mode 100644 src/api/v1alpha1/ref_types.go create mode 100644 src/api/v1alpha1/refresolver.go create mode 100644 src/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 src/cmd/main.go create mode 100644 src/config/crd/bases/zitadel.github.com_actions.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_apiapps.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_connections.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_flows.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_machineusers.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_oidcapps.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_organizations.yaml create mode 100644 src/config/crd/bases/zitadel.github.com_projects.yaml create mode 100644 src/config/crd/kustomization.yaml create mode 100644 src/config/crd/kustomizeconfig.yaml create mode 100644 src/config/default/kustomization.yaml create mode 100644 src/config/default/manager_auth_proxy_patch.yaml create mode 100644 src/config/default/manager_config_patch.yaml create mode 100644 src/config/manager/kustomization.yaml create mode 100644 src/config/manager/manager.yaml create mode 100644 src/config/rbac/action_editor_role.yaml create mode 100644 src/config/rbac/action_viewer_role.yaml create mode 100644 src/config/rbac/apiapp_editor_role.yaml create mode 100644 src/config/rbac/apiapp_viewer_role.yaml create mode 100644 src/config/rbac/auth_proxy_client_clusterrole.yaml create mode 100644 src/config/rbac/auth_proxy_role.yaml create mode 100644 src/config/rbac/auth_proxy_role_binding.yaml create mode 100644 src/config/rbac/auth_proxy_service.yaml create mode 100644 src/config/rbac/cluster_editor_role.yaml create mode 100644 src/config/rbac/connection_editor_role.yaml create mode 100644 src/config/rbac/connection_viewer_role.yaml create mode 100644 src/config/rbac/flow_editor_role.yaml create mode 100644 src/config/rbac/flow_viewer_role.yaml create mode 100644 src/config/rbac/kustomization.yaml create mode 100644 src/config/rbac/leader_election_role.yaml create mode 100644 src/config/rbac/leader_election_role_binding.yaml create mode 100644 src/config/rbac/machineuser_editor_role.yaml create mode 100644 src/config/rbac/machineuser_viewer_role.yaml create mode 100644 src/config/rbac/oidcapp_editor_role.yaml create mode 100644 src/config/rbac/oidcapp_viewer_role.yaml create mode 100644 src/config/rbac/organization_editor_role.yaml create mode 100644 src/config/rbac/organization_viewer_role.yaml create mode 100644 src/config/rbac/project_editor_role.yaml create mode 100644 src/config/rbac/project_viewer_role.yaml create mode 100644 src/config/rbac/role.yaml create mode 100644 src/config/rbac/role_binding.yaml create mode 100644 src/config/rbac/service_account.yaml create mode 100644 src/config/samples/kustomization.yaml create mode 100644 src/config/samples/zitadel_v1alpha1_connection.yaml create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/hack/boilerplate.go.txt create mode 100644 src/internal/controller/action_controller.gold create mode 100644 src/internal/controller/action_controller_finalizer.gold create mode 100644 src/internal/controller/apiapp_controller.gold create mode 100644 src/internal/controller/apiapp_controller_finalizer.gold create mode 100644 src/internal/controller/connection_controller.go create mode 100644 src/internal/controller/connection_controller_finalizer.go create mode 100644 src/internal/controller/flow_controller.gold create mode 100644 src/internal/controller/flow_controller_finalizer.gold create mode 100644 src/internal/controller/instance_controller_test.gold create mode 100644 src/internal/controller/machineuser_controller.gold create mode 100644 src/internal/controller/machineuser_controller_finalizer.gold create mode 100644 src/internal/controller/oidcapp_controller.gold create mode 100644 src/internal/controller/oidcapp_controller_finalizer.gold create mode 100644 src/internal/controller/organization_controller.gold create mode 100644 src/internal/controller/organization_controller_finalizer.gold create mode 100644 src/internal/controller/project_controller.gold create mode 100644 src/internal/controller/project_controller_finalizer.gold create mode 100644 src/internal/controller/suite_test.go create mode 100644 src/pkg/builder/builder.go create mode 100644 src/pkg/builder/labels/labels.go create mode 100644 src/pkg/builder/metadata/metadata.go create mode 100644 src/pkg/builder/secret_builder.go create mode 100644 src/pkg/condition/condition.go create mode 100644 src/pkg/condition/pat.go create mode 100644 src/pkg/condition/ready.go create mode 100644 src/pkg/controller/core/controller.go create mode 100644 src/pkg/controller/core/finalizer.go create mode 100644 src/pkg/controller/core/types.go create mode 100644 src/pkg/zitadel/zitadel.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3bc14b2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake ./build/# diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9f7c8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv diff --git a/build/flake.lock b/build/flake.lock new file mode 100644 index 0000000..abf6f02 --- /dev/null +++ b/build/flake.lock @@ -0,0 +1,73 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 0, + "narHash": "sha256-7Fu7oazPoYCbDzb9k8D/DdbKrC3aU1zlnc39Y8jy/s8=", + "path": "/nix/store/m4wcdchjxw2fdyzjp8i6irpc613pchkr-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1743448293, + "narHash": "sha256-bmEPmSjJakAp/JojZRrUvNcDX2R5/nuX6bm+seVaGhs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "77b584d61ff80b4cef9245829a6f1dfad5afdfa3", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/build/flake.nix b/build/flake.nix new file mode 100644 index 0000000..7306168 --- /dev/null +++ b/build/flake.nix @@ -0,0 +1,52 @@ +{ + description = "Zitadel Resources Operator"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs-unstable.url = "nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + unstable = nixpkgs-unstable.legacyPackages.${system}; + pkgs = nixpkgs.legacyPackages.${system}; + package = unstable.buildGoModule { + pname = "zitadel-resources-operator"; + version = "0.0.0"; + src = ../src; + doCheck = false; + vendorHash = "sha256-HEXIHASdDC7chG9uF56f6pvZPVbxYs/fWFytDz6CAf4="; + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + dir="$GOPATH/bin" + [ -e "$dir" ] && cp -r $dir/cmd $out/manager + + runHook postInstall + ''; + }; + dockerPackage = pkgs.dockerTools.buildImage { + name = "zitadel-resources-operator"; + fromImageName = "gcr.io/distroless/static"; + fromImageTag = "nonroot"; + copyToRoot = pkgs.buildEnv { + name = "operator"; + paths = [ package ]; + pathsToLink = [ "/" ]; + }; + config = { + Cmd = [ "/manager" ]; + WorkingDir = "/"; + User = "65532:65532"; + }; + }; + in with pkgs; { + packages.default = package; + packages.dockerImage = dockerPackage; + devShells.default = mkShell { + buildInputs = [ nixfmt unstable.gopls operator-sdk unstable.go ]; + }; + }); +} diff --git a/build/push-image.sh b/build/push-image.sh new file mode 100755 index 0000000..cbf89fb --- /dev/null +++ b/build/push-image.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -xeuo pipefail + +# Setup client certificate for docker registry login +mkdir -p /.docker +mkdir -p /etc/docker/certs.d/$DOCKERREGISTRY_URL +echo $DOCKERREGISTRY_CACERT +(umask 077 ; echo $DOCKERREGISTRY_CACERT | base64 -d > /.docker/ca.pem) +(umask 077 ; echo $DOCKERREGISTRY_CACERT | base64 -d > /etc/docker/certs.d/$DOCKERREGISTRY_URL/ca.crt) #Don't ask why this is needed twice. +(umask 077 ; echo $DOCKERREGISTRY_CLIENTCERT | base64 -d > /etc/docker/certs.d/$DOCKERREGISTRY_URL/client.cert) +(umask 077 ; echo $DOCKERREGISTRY_CLIENTKEY | base64 -d > /etc/docker/certs.d/$DOCKERREGISTRY_URL/client.key) + +docker --tls login -u $DOCKERREGISTRY_USER -p $DOCKERREGISTRY_PASSWORD $DOCKERREGISTRY_URL + +export DOCKER_HOST=$DOCKERDAEMON_ADDRESS #Setup docker to use a specific daemon + +BUILD_IMAGE_NAME=$(ls images | tee /dev/stderr | head -n 1) + +IMAGE_ID=$( + docker load --input "images/$BUILD_IMAGE_NAME" | + sed -nr 's/^Loaded image: (.*)$/\1/p' | + xargs -I{} docker image ls "{}" --format="{{.ID}}" | + tee /dev/stderr +) + +DOCKER_IMAGE_NAME=$DOCKERREGISTRY_URL/$BITBUCKET_REPO_SLUG +VERSION=$BITBUCKET_BUILD_NUMBER + +if [[ "${BITBUCKET_BRANCH:-""}" == "master" ]]; then + LATEST="latest" +else + unset LATEST +fi + +escapeTag(){ echo "${1//[^a-zA-Z0-9._\-]/-}"; } + +tagPush(){ + if [ -n "$1" ]; then + local tag=$(escapeTag "$1") + docker tag "$IMAGE_ID" "$DOCKER_IMAGE_NAME:$tag" && docker push "$DOCKER_IMAGE_NAME:$tag" + fi +} + +tagRemove(){ + if [ -n "$1" ]; then + local tag=$(escapeTag "$1") + docker rmi "$DOCKER_IMAGE_NAME:$tag" + fi +} + +set +u + +tagPush "$VERSION" +tagPush "$BITBUCKET_BRANCH" +tagPush "$BITBUCKET_TAG" +tagPush "$BITBUCKET_COMMIT" +tagPush "$LATEST" + +tagRemove "$VERSION" +tagRemove "$BITBUCKET_BRANCH" +tagRemove "$BITBUCKET_TAG" +tagRemove "$BITBUCKET_COMMIT" +tagRemove "$LATEST" diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..0f04682 --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..e917e5c --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..ef4cfaf --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,33 @@ +# Build the manager binary +FROM golang:1.19 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/src/Makefile b/src/Makefile new file mode 100644 index 0000000..145ab12 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,291 @@ +# 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 +# github.com/src-bundle:$VERSION and github.com/src-catalog:$VERSION. +IMAGE_TAG_BASE ?= github.com/src + +# 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.26.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 + +# 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 commands 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 ./... -coverprofile cover.out + +##@ 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 built 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: test ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## 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 + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross + +##@ 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: ## 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 - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v4.5.7 +CONTROLLER_TOOLS_VERSION ?= v0.17.3 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + +.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 = ./bin/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) + +HELMIFY ?= $(LOCALBIN)/helmify + +.PHONY: helmify +helmify: $(HELMIFY) ## Download helmify locally if necessary. +$(HELMIFY): $(LOCALBIN) + test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest + +helm: manifests kustomize helmify + $(KUSTOMIZE) build config/default | $(HELMIFY) -crd-dir zitadel-resources-operator diff --git a/src/PROJECT b/src/PROJECT new file mode 100644 index 0000000..3e9ccac --- /dev/null +++ b/src/PROJECT @@ -0,0 +1,87 @@ +# 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: github.com +layout: +- go.kubebuilder.io/v4 +plugins: + manifests.sdk.operatorframework.io/v2: {} + scorecard.sdk.operatorframework.io/v2: {} +projectName: src +repo: github.com/HaimKortovich/zitadel-resources-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: Organization + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: Project + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: OIDCApp + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: MachineUser + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: APIApp + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: Action + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: Flow + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +- api: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: github.com + group: zitadel + kind: Connection + path: github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..59e68c3 --- /dev/null +++ b/src/README.md @@ -0,0 +1,94 @@ +# src +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -f config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/src:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/src:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller from the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), +which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**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 2024. + +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/src/api/v1alpha1/action_types.go b/src/api/v1alpha1/action_types.go new file mode 100644 index 0000000..2fc6580 --- /dev/null +++ b/src/api/v1alpha1/action_types.go @@ -0,0 +1,100 @@ +/* +Copyright 2024. + +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 ( + "context" + "k8s.io/apimachinery/pkg/api/meta" + 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. + +// ActionSpec defines the desired state of Action +type ActionSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + OrganizationRef OrganizationRef `json:"organizationRef"` + Script string `json:"script"` + // +kubebuilder:default=true + AllowedToFail bool `json:"allowedToFail"` + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + Timeout *metav1.Duration `json:"timeout"` +} + +// ActionStatus defines the observed state of Action +type ActionStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + // +kubebuilder:default="" + ActionId string `json:"actionId"` +} + +func (d *ActionStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Action is the Schema for the actions API +type Action struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ActionSpec `json:"spec,omitempty"` + Status ActionStatus `json:"status,omitempty"` +} + +func (d *Action) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Action) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Action) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return &org.Spec.ConnectionRef, nil +} + +//+kubebuilder:object:root=true + +// ActionList contains a list of Action +type ActionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Action `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Action{}, &ActionList{}) +} diff --git a/src/api/v1alpha1/apiapp_types.go b/src/api/v1alpha1/apiapp_types.go new file mode 100644 index 0000000..a0d2d5f --- /dev/null +++ b/src/api/v1alpha1/apiapp_types.go @@ -0,0 +1,122 @@ +/* +Copyright 2024. + +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 ( + "context" + + "k8s.io/apimachinery/pkg/api/meta" + 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. + +// APIAppSpec defines the desired state of APIApp +type APIAppSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + ProjectRef ProjectRef `json:"projectRef"` + // +kubebuilder:validation:Enum=API_AUTH_METHOD_TYPE_BASIC;API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + AuthMethodType string `json:"authMethodType"` +} + +// APIAppStatus defines the observed state of APIApp +type APIAppStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + // +kubebuilder:default="" + AppId string `json:"appId"` + // +kubebuilder:default="" + KeyId string `json:"keyId"` + // +kubebuilder:default="" + ClientId string `json:"clientId"` +} + +func (d *APIAppStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// APIApp is the Schema for the apiapps API +type APIApp struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec APIAppSpec `json:"spec,omitempty"` + Status APIAppStatus `json:"status,omitempty"` +} + +func (d *APIApp) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *APIApp) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *APIApp) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) + if err != nil { + return nil, err + } + org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return &org.Spec.ConnectionRef, nil +} +func (d *APIApp) Organization(ctx context.Context, refresolver *RefResolver) (*Organization, error) { + project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) + if err != nil { + return nil, err + } + org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return org, nil +} + +func (d *APIApp) Project(ctx context.Context, refresolver *RefResolver) (*Project, error) { + project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) + if err != nil { + return nil, err + } + return project, nil +} + +//+kubebuilder:object:root=true + +// APIAppList contains a list of APIApp +type APIAppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []APIApp `json:"items"` +} + +func init() { + SchemeBuilder.Register(&APIApp{}, &APIAppList{}) +} diff --git a/src/api/v1alpha1/condition_types.go b/src/api/v1alpha1/condition_types.go new file mode 100644 index 0000000..a16473e --- /dev/null +++ b/src/api/v1alpha1/condition_types.go @@ -0,0 +1,32 @@ +package v1alpha1 + +const ( + ConditionTypeReady string = "Ready" + ConditionTypePATUpToDate string = "PATUpToDate" + + ConditionReasonRolesChanged string = "RolesChanged" + ConditionReasonPATUpToDate string = "UpToDate" + + ConditionReasonDeploymentNotReady string = "DeploymentNotReady" + ConditionReasonDeploymentReady string = "DeploymentReady" + ConditionReasonRestoreBackup string = "RestoreBackup" + + ConditionReasonRestoreNotComplete string = "RestoreNotComplete" + ConditionReasonRestoreComplete string = "RestoreComplete" + + ConditionReasonJobComplete string = "JobComplete" + ConditionReasonJobSuspended string = "JobSuspended" + ConditionReasonJobFailed string = "JobFailed" + ConditionReasonJobRunning string = "JobRunning" + + ConditionReasonCronJobScheduled string = "CronJobScheduled" + ConditionReasonCronJobFailed string = "CronJobScheduled" + ConditionReasonCronJobRunning string = "CronJobRunning" + ConditionReasonCronJobSuccess string = "CronJobSucess" + + ConditionReasonConnectionFailed string = "ConnectionFailed" + + ConditionReasonCreated string = "Created" + ConditionReasonHealthy string = "Healthy" + ConditionReasonFailed string = "Failed" +) diff --git a/src/api/v1alpha1/connection_types.go b/src/api/v1alpha1/connection_types.go new file mode 100644 index 0000000..7e0b201 --- /dev/null +++ b/src/api/v1alpha1/connection_types.go @@ -0,0 +1,122 @@ +/* +Copyright 2024. + +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" + + "context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" +) + +// 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. +type PAT struct { + TokenSecretKey corev1.SecretKeySelector `json:"tokenSecretKey"` +} + +type JWT struct { + JWTSecretKey corev1.SecretKeySelector `json:"jwtSecretKey"` + Scopes []string `json:"scopes"` +} + +type UserPassword struct { + Username string `json:"username"` + PasswordSecretKey corev1.SecretKeySelector `json:"passwordSecretKey"` + Scopes []string `json:"scopes"` +} + +// +kubebuilder:validation:XValidation:rule="[has(self.pat), has(self.password), has(self.jwt)].filter(x, x).size() == 1",message="exactly one of pat, password, or jwt must be specified" +type Authentication struct { + PAT *PAT `json:"pat,omitempty"` + Password *UserPassword `json:"password,omitempty"` + JWT *JWT `json:"jwt,omitempty"` +} + +// ConnectionSpec defines the desired state of Connection +type ConnectionSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Host string `json:"host"` + Port *uint16 `json:"port,omitempty"` + // +kubebuilder:default=true + Secure bool `json:"secure"` + // +kubebuilder:default=false + InsecureSkipVerifyTLS bool `json:"insecureSkipVerifyTLS"` + Authentication Authentication `json:"authentication"` +} + +// ConnectionStatus defines the observed state of Connection +type ConnectionStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // Conditions for the Connection object. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +func (d *ConnectionStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +func (d *Connection) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Connection) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Connection) ConnectionRef(_ context.Context, _ *RefResolver) (*ConnectionRef, error) { + return &ConnectionRef{ + ObjectReference: corev1.ObjectReference{ + Kind: d.Kind, + Namespace: d.Namespace, + Name: d.Name, + }, + }, nil +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Connection is the Schema for the connections API +type Connection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ConnectionSpec `json:"spec,omitempty"` + Status ConnectionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ConnectionList contains a list of Connection +type ConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Connection `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Connection{}, &ConnectionList{}) +} diff --git a/src/api/v1alpha1/flow_types.go b/src/api/v1alpha1/flow_types.go new file mode 100644 index 0000000..1e4e123 --- /dev/null +++ b/src/api/v1alpha1/flow_types.go @@ -0,0 +1,97 @@ +/* +Copyright 2024. + +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 ( + "context" + "k8s.io/apimachinery/pkg/api/meta" + 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. + +// FlowSpec defines the desired state of Flow +type FlowSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + OrganizationRef OrganizationRef `json:"organizationRef"` + // +kubebuilder:validation:Enum=FLOW_TYPE_EXTERNAL_AUTHENTICATION;"1";"2";"3";"4" + FlowType string `json:"flowType"` + // +kubebuilder:validation:Enum=TRIGGER_TYPE_POST_AUTHENTICATION;TRIGGER_TYPE_PRE_CREATION;TRIGGER_TYPE_POST_CREATION;TRIGGER_TYPE_POST_AUTHENTICATION;TRIGGER_TYPE_PRE_CREATION;TRIGGER_TYPE_POST_CREATION;"1";"2";"3";"4";"5";"6" + TriggerType string `json:"triggerType"` + ActionRefs []ActionRef `json:"actionRefs"` +} + +// FlowStatus defines the observed state of Flow +type FlowStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +func (d *FlowStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Flow is the Schema for the flows API +type Flow struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FlowSpec `json:"spec,omitempty"` + Status FlowStatus `json:"status,omitempty"` +} + +func (d *Flow) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Flow) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Flow) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return &org.Spec.ConnectionRef, nil +} + +//+kubebuilder:object:root=true + +// FlowList contains a list of Flow +type FlowList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Flow `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Flow{}, &FlowList{}) +} diff --git a/src/api/v1alpha1/groupversion_info.go b/src/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..003c6a1 --- /dev/null +++ b/src/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +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 zitadel v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=zitadel.github.com +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: "zitadel.github.com", 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/src/api/v1alpha1/machineuser_types.go b/src/api/v1alpha1/machineuser_types.go new file mode 100644 index 0000000..655a9a6 --- /dev/null +++ b/src/api/v1alpha1/machineuser_types.go @@ -0,0 +1,156 @@ +/* +Copyright 2024. + +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 ( + "context" + + "k8s.io/apimachinery/pkg/api/meta" + 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. + +type Authorization struct { + ProjectRef ProjectRef `json:"projectRef"` + RoleKeys []string `json:"roleKeys,omitempty"` +} + +type OrganizationResource struct { + OrgID string `json:"orgId"` +} + +type ProjectResource struct { + ProjectID string `json:"projectId"` +} + +type ProjectGrantResource struct { + ProjectID string `json:"projectId"` + OrgID string `json:"orgId"` +} + +type ConnectionResource struct{} + +// +kubebuilder:validation:XValidation:rule="[has(self.organization), has(self.connection), has(self.project), has(self.projectGrant)].filter(x, x).size() == 1",message="exactly one of organization, connection, project, or projectGrant must be specified" +type Resource struct { + // +optional + Organization *OrganizationResource `json:"organization,omitempty"` + // +optional + Connection *ConnectionResource `json:"connection,omitempty"` + // +optional + Project *ProjectResource `json:"project,omitempty"` + // +optional + ProjectGrant *ProjectGrantResource `json:"projectGrant,omitempty"` +} + +type InternalPermissions struct { + Resource Resource `json:"resource"` + // +kubebuilder:validation:MaxItems=50 + Roles []string `json:"roles,omitempty"` +} + +// MachineUserSpec defines the desired state of MachineUser +type MachineUserSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + OrganizationRef OrganizationRef `json:"organizationRef" webhook:"inmutable"` + // +kubebuilder:validation:Enum=ACCESS_TOKEN_TYPE_BEARER;ACCESS_TOKEN_TYPE_JWT + AccessTokenType string `json:"accessTokenType"` + Authorizations []Authorization `json:"authorizations,omitempty"` + // +kubebuilder:validation:MaxItems=100 + InternalPermissions []InternalPermissions `json:"internalPermissions,omitempty"` + Metadata []map[string]string `json:"metadata,omitempty"` + Username string `json:"username"` +} + +// MachineUserStatus defines the observed state of MachineUser +type MachineUserStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + UserId *string `json:"userId,omitempty"` + KeyId *string `json:"keyId,omitempty"` + PATId *string `json:"patId,omitempty"` +} + +func (d *MachineUserStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +func (d *MachineUserStatus) GetConditionStatus(conditionType string) bool { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + return meta.IsStatusConditionTrue(d.Conditions, conditionType) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// MachineUser is the Schema for the machineusers API +type MachineUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MachineUserSpec `json:"spec,omitempty"` + Status MachineUserStatus `json:"status,omitempty"` +} + +func (d *MachineUser) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *MachineUser) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *MachineUser) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return &org.Spec.ConnectionRef, nil +} + +func (d *MachineUser) PatSecretName() string { + return d.Name + "-pat-secret" +} + +func (d *MachineUser) JWTSecretName() string { + return d.Name + "-machinekey-secret" +} + +//+kubebuilder:object:root=true + +// MachineUserList contains a list of MachineUser +type MachineUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MachineUser `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MachineUser{}, &MachineUserList{}) +} diff --git a/src/api/v1alpha1/oidcapp_types.go b/src/api/v1alpha1/oidcapp_types.go new file mode 100644 index 0000000..897e079 --- /dev/null +++ b/src/api/v1alpha1/oidcapp_types.go @@ -0,0 +1,149 @@ +/* +Copyright 2024. + +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 ( + "context" + "k8s.io/apimachinery/pkg/api/meta" + 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. + +// +kubebuilder:validation:Enum=OIDC_RESPONSE_TYPE_CODE;OIDC_RESPONSE_TYPE_ID_TOKEN;OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN +type ResponseType string + +// +kubebuilder:validation:Enum=OIDC_GRANT_TYPE_AUTHORIZATION_CODE;OIDC_GRANT_TYPE_IMPLICIT;OIDC_GRANT_TYPE_REFRESH_TOKEN;OIDC_GRANT_TYPE_DEVICE_CODE;OIDC_GRANT_TYPE_TOKEN_EXCHANGE +type GrantType string + +// OIDCAppSpec defines the desired state of OIDCApp +type OIDCAppSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + ProjectRef ProjectRef `json:"projectRef"` + OIDCAppName string `json:"oidcAppName"` + RedirectUris []string `json:"redirectUris"` + ResponseTypes []ResponseType `json:"responseTypes"` + GrantTypes []GrantType `json:"grantTypes"` + // +kubebuilder:validation:Enum=OIDC_APP_TYPE_WEB;OIDC_APP_TYPE_USER_AGENT;OIDC_APP_TYPE_NATIVE + AppType string `json:"appType"` + // +kubebuilder:validation:Enum=OIDC_AUTH_METHOD_TYPE_BASIC;OIDC_AUTH_METHOD_TYPE_POST;OIDC_AUTH_METHOD_TYPE_NONE;OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + AuthMethodType string `json:"authMethodType"` + PostLogoutRedirectUris []string `json:"postLogoutRedirectUris"` + DevMode bool `json:"devMode"` + // +kubebuilder:validation:Enum=OIDC_TOKEN_TYPE_BEARER;OIDC_TOKEN_TYPE_JWT + AccessTokenType string `json:"accessTokenType"` + AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion"` + IdTokenRoleAssertion bool `json:"idTokenRoleAssertion"` + IdTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion"` + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + ClockSkew *metav1.Duration `json:"clockSkew"` + // +optional + AdditionalOrigins []string `json:"additionalOrigins"` + SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage"` + BackChannelLogoutUri string `json:"backChannelLogoutUri,omitempty"` +} + +// OIDCAppStatus defines the observed state of OIDCApp +type OIDCAppStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + AppId *string `json:"appId"` + ClientId *string `json:"clientId,omitempty"` +} + +func (d *OIDCAppStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// OIDCApp is the Schema for the oidcapps API +type OIDCApp struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OIDCAppSpec `json:"spec,omitempty"` + Status OIDCAppStatus `json:"status,omitempty"` +} + +func (d *OIDCApp) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *OIDCApp) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *OIDCApp) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) + if err != nil { + return nil, err + } + + org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + + return &org.Spec.ConnectionRef, nil +} +func (d *OIDCApp) Organization(ctx context.Context, refresolver *RefResolver) (*Organization, error) { + project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) + if err != nil { + return nil, err + } + org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return org, nil +} + +func (d *OIDCApp) Project(ctx context.Context, refresolver *RefResolver) (*Project, error) { + project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) + if err != nil { + return nil, err + } + return project, nil +} + +func (d *OIDCApp) ClientSecretName() string { + return d.Name + "-client-secret" +} + +//+kubebuilder:object:root=true + +// OIDCAppList contains a list of OIDCApp +type OIDCAppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OIDCApp `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OIDCApp{}, &OIDCAppList{}) +} diff --git a/src/api/v1alpha1/organization_types.go b/src/api/v1alpha1/organization_types.go new file mode 100644 index 0000000..e03fcc7 --- /dev/null +++ b/src/api/v1alpha1/organization_types.go @@ -0,0 +1,92 @@ +/* +Copyright 2024. + +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 ( + "context" + + "k8s.io/apimachinery/pkg/api/meta" + 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. + +// OrganizationSpec defines the desired state of Organization +type OrganizationSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + ConnectionRef ConnectionRef `json:"connectionRef" webhook:"inmutable"` + OrganzationName string `json:"organizationName"` +} + +// OrganizationStatus defines the observed state of Organization +type OrganizationStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // Conditions for the Database object. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + OrganizationId *string `json:"organizationId,omitempty"` +} + +func (d *OrganizationStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Organization is the Schema for the organizations API +type Organization struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OrganizationSpec `json:"spec,omitempty"` + Status OrganizationStatus `json:"status,omitempty"` +} + +func (d *Organization) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Organization) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Organization) ConnectionRef(_ context.Context, _ *RefResolver) (*ConnectionRef, error) { + return &d.Spec.ConnectionRef, nil +} + +//+kubebuilder:object:root=true + +// OrganizationList contains a list of Organization +type OrganizationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Organization `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Organization{}, &OrganizationList{}) +} diff --git a/src/api/v1alpha1/project_types.go b/src/api/v1alpha1/project_types.go new file mode 100644 index 0000000..7025936 --- /dev/null +++ b/src/api/v1alpha1/project_types.go @@ -0,0 +1,117 @@ +/* +Copyright 2024. + +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 ( + "context" + + "k8s.io/apimachinery/pkg/api/meta" + 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. + +type Role struct { + Key string `json:"key"` + DisplayName string `json:"displayName"` + Group string `json:"group"` +} +type Grant struct { + OrganizationRef OrganizationRef `json:"organizationRef"` + RoleKeys []string `json:"roleKeys"` +} + +// ProjectSpec defines the desired state of Project +type ProjectSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // https://zitadel.com/docs/apis/resources/mgmt/management-service-add-project + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + OrganizationRef OrganizationRef `json:"organizationRef"` + // +optional + Roles []Role `json:"roles"` + // +optional + Grants []Grant `json:"grants"` + // +optional + ProjectRoleAssertion bool `json:"projectRoleAssertion,omitempty"` + // +optional + ProjectRoleCheck bool `json:"projectRoleCheck,omitempty"` + // +optional + HasProjectCheck bool `json:"hasProjectCheck,omitempty"` + ProjectName string `json:"projectName"` +} + +// ProjectStatus defines the observed state of Project +type ProjectStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // Conditions for the Database object. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + ProjectId *string `json:"projectId,omitempty"` +} + +func (d *ProjectStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Project is the Schema for the projects API +type Project struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProjectSpec `json:"spec,omitempty"` + Status ProjectStatus `json:"status,omitempty"` +} + +func (d *Project) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Project) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Project) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + return &org.Spec.ConnectionRef, nil +} + +//+kubebuilder:object:root=true + +// ProjectList contains a list of Project +type ProjectList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Project `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Project{}, &ProjectList{}) +} diff --git a/src/api/v1alpha1/ref_types.go b/src/api/v1alpha1/ref_types.go new file mode 100644 index 0000000..6c7b6e1 --- /dev/null +++ b/src/api/v1alpha1/ref_types.go @@ -0,0 +1,35 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" +) + +type ConnectionRef struct { + // ObjectReference is a reference to a object. + // +operator-sdk:csv:customresourcedefinitions:type=spec + corev1.ObjectReference `json:",inline"` +} + +type OIDCAppRef struct { + // ObjectReference is a reference to a object. + // +operator-sdk:csv:customresourcedefinitions:type=spec + corev1.ObjectReference `json:",inline"` +} + +type OrganizationRef struct { + // ObjectReference is a reference to a object. + // +operator-sdk:csv:customresourcedefinitions:type=spec + corev1.ObjectReference `json:",inline"` +} + +type ProjectRef struct { + // ObjectReference is a reference to a object. + // +operator-sdk:csv:customresourcedefinitions:type=spec + corev1.ObjectReference `json:",inline"` +} + +type ActionRef struct { + // ObjectReference is a reference to a object. + // +operator-sdk:csv:customresourcedefinitions:type=spec + corev1.ObjectReference `json:",inline"` +} diff --git a/src/api/v1alpha1/refresolver.go b/src/api/v1alpha1/refresolver.go new file mode 100644 index 0000000..ca889b0 --- /dev/null +++ b/src/api/v1alpha1/refresolver.go @@ -0,0 +1,143 @@ +package v1alpha1 + +import ( + "context" + "fmt" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// +kubebuilder:object:generate=false +type RefResolver struct { + client client.Client +} + +func NewRefResolver(client client.Client) *RefResolver { + return &RefResolver{ + client: client, + } +} + +func (r *RefResolver) OIDCAppRef(ctx context.Context, ref *OIDCAppRef, + namespace string) (*OIDCApp, error) { + if ref.Kind != "" && ref.Kind != "OIDCApp" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + } + + key := types.NamespacedName{ + Name: ref.Name, + Namespace: namespace, + } + if ref.Namespace != "" { + key.Namespace = ref.Namespace + } + + var zitadel OIDCApp + if err := r.client.Get(ctx, key, &zitadel); err != nil { + return nil, err + } + return &zitadel, nil +} + +func (r *RefResolver) ActionRef(ctx context.Context, ref *ActionRef, + namespace string) (*Action, error) { + if ref.Kind != "" && ref.Kind != "Action" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + } + + key := types.NamespacedName{ + Name: ref.Name, + Namespace: namespace, + } + if ref.Namespace != "" { + key.Namespace = ref.Namespace + } + + var zitadel Action + if err := r.client.Get(ctx, key, &zitadel); err != nil { + return nil, err + } + return &zitadel, nil +} + +func (r *RefResolver) ProjectRef(ctx context.Context, ref *ProjectRef, + namespace string) (*Project, error) { + if ref.Kind != "" && ref.Kind != "Project" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + } + + key := types.NamespacedName{ + Name: ref.Name, + Namespace: namespace, + } + if ref.Namespace != "" { + key.Namespace = ref.Namespace + } + + var zitadel Project + if err := r.client.Get(ctx, key, &zitadel); err != nil { + return nil, err + } + return &zitadel, nil +} + +func (r *RefResolver) OrganizationRef(ctx context.Context, ref *OrganizationRef, + namespace string) (*Organization, error) { + if ref.Kind != "" && ref.Kind != "Organization" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + } + + key := types.NamespacedName{ + Name: ref.Name, + Namespace: namespace, + } + if ref.Namespace != "" { + key.Namespace = ref.Namespace + } + + var zitadel Organization + if err := r.client.Get(ctx, key, &zitadel); err != nil { + return nil, err + } + return &zitadel, nil +} + +func (r *RefResolver) ConnectionRef(ctx context.Context, ref *ConnectionRef, namespace string) (*Connection, error) { + if ref.Kind != "" && ref.Kind != "Connection" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + } + key := types.NamespacedName{ + Name: ref.Name, + Namespace: namespace, + } + if ref.Namespace != "" { + key.Namespace = ref.Namespace + } + + var connection Connection + if err := r.client.Get(ctx, key, &connection); err != nil { + return nil, err + } + return &connection, nil +} + +func (r *RefResolver) SecretKeyRef(ctx context.Context, selector corev1.SecretKeySelector, + namespace string) (string, error) { + nn := types.NamespacedName{ + Name: selector.Name, + Namespace: namespace, + } + var secret v1.Secret + if err := r.client.Get(ctx, nn, &secret); err != nil { + return "", fmt.Errorf("error getting secret: %v", err) + } + + data, ok := secret.Data[selector.Key] + if !ok { + return "", fmt.Errorf("secret key \"%s\" not found", selector.Key) + } + + return string(data), nil +} diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..6a8e9f0 --- /dev/null +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,1262 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024. + +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 ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + 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 *APIApp) DeepCopyInto(out *APIApp) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIApp. +func (in *APIApp) DeepCopy() *APIApp { + if in == nil { + return nil + } + out := new(APIApp) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIApp) 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 *APIAppList) DeepCopyInto(out *APIAppList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIApp, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIAppList. +func (in *APIAppList) DeepCopy() *APIAppList { + if in == nil { + return nil + } + out := new(APIAppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIAppList) 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 *APIAppSpec) DeepCopyInto(out *APIAppSpec) { + *out = *in + out.ProjectRef = in.ProjectRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIAppSpec. +func (in *APIAppSpec) DeepCopy() *APIAppSpec { + if in == nil { + return nil + } + out := new(APIAppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIAppStatus) DeepCopyInto(out *APIAppStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIAppStatus. +func (in *APIAppStatus) DeepCopy() *APIAppStatus { + if in == nil { + return nil + } + out := new(APIAppStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Action) DeepCopyInto(out *Action) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Action. +func (in *Action) DeepCopy() *Action { + if in == nil { + return nil + } + out := new(Action) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Action) 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 *ActionList) DeepCopyInto(out *ActionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Action, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionList. +func (in *ActionList) DeepCopy() *ActionList { + if in == nil { + return nil + } + out := new(ActionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActionList) 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 *ActionRef) DeepCopyInto(out *ActionRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionRef. +func (in *ActionRef) DeepCopy() *ActionRef { + if in == nil { + return nil + } + out := new(ActionRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionSpec) DeepCopyInto(out *ActionSpec) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionSpec. +func (in *ActionSpec) DeepCopy() *ActionSpec { + if in == nil { + return nil + } + out := new(ActionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionStatus) DeepCopyInto(out *ActionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionStatus. +func (in *ActionStatus) DeepCopy() *ActionStatus { + if in == nil { + return nil + } + out := new(ActionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authentication) DeepCopyInto(out *Authentication) { + *out = *in + if in.PAT != nil { + in, out := &in.PAT, &out.PAT + *out = new(PAT) + (*in).DeepCopyInto(*out) + } + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(UserPassword) + (*in).DeepCopyInto(*out) + } + if in.JWT != nil { + in, out := &in.JWT, &out.JWT + *out = new(JWT) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. +func (in *Authentication) DeepCopy() *Authentication { + if in == nil { + return nil + } + out := new(Authentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authorization) DeepCopyInto(out *Authorization) { + *out = *in + out.ProjectRef = in.ProjectRef + if in.RoleKeys != nil { + in, out := &in.RoleKeys, &out.RoleKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authorization. +func (in *Authorization) DeepCopy() *Authorization { + if in == nil { + return nil + } + out := new(Authorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Connection) DeepCopyInto(out *Connection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Connection. +func (in *Connection) DeepCopy() *Connection { + if in == nil { + return nil + } + out := new(Connection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Connection) 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 *ConnectionList) DeepCopyInto(out *ConnectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Connection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionList. +func (in *ConnectionList) DeepCopy() *ConnectionList { + if in == nil { + return nil + } + out := new(ConnectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ConnectionList) 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 *ConnectionRef) DeepCopyInto(out *ConnectionRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionRef. +func (in *ConnectionRef) DeepCopy() *ConnectionRef { + if in == nil { + return nil + } + out := new(ConnectionRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionResource) DeepCopyInto(out *ConnectionResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionResource. +func (in *ConnectionResource) DeepCopy() *ConnectionResource { + if in == nil { + return nil + } + out := new(ConnectionResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionSpec) DeepCopyInto(out *ConnectionSpec) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(uint16) + **out = **in + } + in.Authentication.DeepCopyInto(&out.Authentication) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSpec. +func (in *ConnectionSpec) DeepCopy() *ConnectionSpec { + if in == nil { + return nil + } + out := new(ConnectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionStatus) DeepCopyInto(out *ConnectionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionStatus. +func (in *ConnectionStatus) DeepCopy() *ConnectionStatus { + if in == nil { + return nil + } + out := new(ConnectionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Flow) DeepCopyInto(out *Flow) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flow. +func (in *Flow) DeepCopy() *Flow { + if in == nil { + return nil + } + out := new(Flow) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Flow) 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 *FlowList) DeepCopyInto(out *FlowList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Flow, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowList. +func (in *FlowList) DeepCopy() *FlowList { + if in == nil { + return nil + } + out := new(FlowList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FlowList) 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 *FlowSpec) DeepCopyInto(out *FlowSpec) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.ActionRefs != nil { + in, out := &in.ActionRefs, &out.ActionRefs + *out = make([]ActionRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowSpec. +func (in *FlowSpec) DeepCopy() *FlowSpec { + if in == nil { + return nil + } + out := new(FlowSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlowStatus) DeepCopyInto(out *FlowStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowStatus. +func (in *FlowStatus) DeepCopy() *FlowStatus { + if in == nil { + return nil + } + out := new(FlowStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Grant) DeepCopyInto(out *Grant) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.RoleKeys != nil { + in, out := &in.RoleKeys, &out.RoleKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Grant. +func (in *Grant) DeepCopy() *Grant { + if in == nil { + return nil + } + out := new(Grant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalPermissions) DeepCopyInto(out *InternalPermissions) { + *out = *in + in.Resource.DeepCopyInto(&out.Resource) + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalPermissions. +func (in *InternalPermissions) DeepCopy() *InternalPermissions { + if in == nil { + return nil + } + out := new(InternalPermissions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWT) DeepCopyInto(out *JWT) { + *out = *in + in.JWTSecretKey.DeepCopyInto(&out.JWTSecretKey) + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWT. +func (in *JWT) DeepCopy() *JWT { + if in == nil { + return nil + } + out := new(JWT) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineUser) DeepCopyInto(out *MachineUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUser. +func (in *MachineUser) DeepCopy() *MachineUser { + if in == nil { + return nil + } + out := new(MachineUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MachineUser) 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 *MachineUserList) DeepCopyInto(out *MachineUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MachineUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserList. +func (in *MachineUserList) DeepCopy() *MachineUserList { + if in == nil { + return nil + } + out := new(MachineUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MachineUserList) 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 *MachineUserSpec) DeepCopyInto(out *MachineUserSpec) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.Authorizations != nil { + in, out := &in.Authorizations, &out.Authorizations + *out = make([]Authorization, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.InternalPermissions != nil { + in, out := &in.InternalPermissions, &out.InternalPermissions + *out = make([]InternalPermissions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserSpec. +func (in *MachineUserSpec) DeepCopy() *MachineUserSpec { + if in == nil { + return nil + } + out := new(MachineUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineUserStatus) DeepCopyInto(out *MachineUserStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.UserId != nil { + in, out := &in.UserId, &out.UserId + *out = new(string) + **out = **in + } + if in.KeyId != nil { + in, out := &in.KeyId, &out.KeyId + *out = new(string) + **out = **in + } + if in.PATId != nil { + in, out := &in.PATId, &out.PATId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserStatus. +func (in *MachineUserStatus) DeepCopy() *MachineUserStatus { + if in == nil { + return nil + } + out := new(MachineUserStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCApp) DeepCopyInto(out *OIDCApp) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCApp. +func (in *OIDCApp) DeepCopy() *OIDCApp { + if in == nil { + return nil + } + out := new(OIDCApp) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OIDCApp) 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 *OIDCAppList) DeepCopyInto(out *OIDCAppList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OIDCApp, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCAppList. +func (in *OIDCAppList) DeepCopy() *OIDCAppList { + if in == nil { + return nil + } + out := new(OIDCAppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OIDCAppList) 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 *OIDCAppRef) DeepCopyInto(out *OIDCAppRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCAppRef. +func (in *OIDCAppRef) DeepCopy() *OIDCAppRef { + if in == nil { + return nil + } + out := new(OIDCAppRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCAppSpec) DeepCopyInto(out *OIDCAppSpec) { + *out = *in + out.ProjectRef = in.ProjectRef + if in.RedirectUris != nil { + in, out := &in.RedirectUris, &out.RedirectUris + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ResponseTypes != nil { + in, out := &in.ResponseTypes, &out.ResponseTypes + *out = make([]ResponseType, len(*in)) + copy(*out, *in) + } + if in.GrantTypes != nil { + in, out := &in.GrantTypes, &out.GrantTypes + *out = make([]GrantType, len(*in)) + copy(*out, *in) + } + if in.PostLogoutRedirectUris != nil { + in, out := &in.PostLogoutRedirectUris, &out.PostLogoutRedirectUris + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ClockSkew != nil { + in, out := &in.ClockSkew, &out.ClockSkew + *out = new(v1.Duration) + **out = **in + } + if in.AdditionalOrigins != nil { + in, out := &in.AdditionalOrigins, &out.AdditionalOrigins + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCAppSpec. +func (in *OIDCAppSpec) DeepCopy() *OIDCAppSpec { + if in == nil { + return nil + } + out := new(OIDCAppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCAppStatus) DeepCopyInto(out *OIDCAppStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AppId != nil { + in, out := &in.AppId, &out.AppId + *out = new(string) + **out = **in + } + if in.ClientId != nil { + in, out := &in.ClientId, &out.ClientId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCAppStatus. +func (in *OIDCAppStatus) DeepCopy() *OIDCAppStatus { + if in == nil { + return nil + } + out := new(OIDCAppStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Organization) DeepCopyInto(out *Organization) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Organization. +func (in *Organization) DeepCopy() *Organization { + if in == nil { + return nil + } + out := new(Organization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Organization) 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 *OrganizationList) DeepCopyInto(out *OrganizationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Organization, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationList. +func (in *OrganizationList) DeepCopy() *OrganizationList { + if in == nil { + return nil + } + out := new(OrganizationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OrganizationList) 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 *OrganizationRef) DeepCopyInto(out *OrganizationRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationRef. +func (in *OrganizationRef) DeepCopy() *OrganizationRef { + if in == nil { + return nil + } + out := new(OrganizationRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OrganizationResource) DeepCopyInto(out *OrganizationResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationResource. +func (in *OrganizationResource) DeepCopy() *OrganizationResource { + if in == nil { + return nil + } + out := new(OrganizationResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OrganizationSpec) DeepCopyInto(out *OrganizationSpec) { + *out = *in + out.ConnectionRef = in.ConnectionRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationSpec. +func (in *OrganizationSpec) DeepCopy() *OrganizationSpec { + if in == nil { + return nil + } + out := new(OrganizationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OrganizationStatus) DeepCopyInto(out *OrganizationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.OrganizationId != nil { + in, out := &in.OrganizationId, &out.OrganizationId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationStatus. +func (in *OrganizationStatus) DeepCopy() *OrganizationStatus { + if in == nil { + return nil + } + out := new(OrganizationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PAT) DeepCopyInto(out *PAT) { + *out = *in + in.TokenSecretKey.DeepCopyInto(&out.TokenSecretKey) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PAT. +func (in *PAT) DeepCopy() *PAT { + if in == nil { + return nil + } + out := new(PAT) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Project) DeepCopyInto(out *Project) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Project. +func (in *Project) DeepCopy() *Project { + if in == nil { + return nil + } + out := new(Project) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Project) 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 *ProjectGrantResource) DeepCopyInto(out *ProjectGrantResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectGrantResource. +func (in *ProjectGrantResource) DeepCopy() *ProjectGrantResource { + if in == nil { + return nil + } + out := new(ProjectGrantResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectList) DeepCopyInto(out *ProjectList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Project, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectList. +func (in *ProjectList) DeepCopy() *ProjectList { + if in == nil { + return nil + } + out := new(ProjectList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProjectList) 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 *ProjectRef) DeepCopyInto(out *ProjectRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectRef. +func (in *ProjectRef) DeepCopy() *ProjectRef { + if in == nil { + return nil + } + out := new(ProjectRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectResource) DeepCopyInto(out *ProjectResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectResource. +func (in *ProjectResource) DeepCopy() *ProjectResource { + if in == nil { + return nil + } + out := new(ProjectResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]Role, len(*in)) + copy(*out, *in) + } + if in.Grants != nil { + in, out := &in.Grants, &out.Grants + *out = make([]Grant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSpec. +func (in *ProjectSpec) DeepCopy() *ProjectSpec { + if in == nil { + return nil + } + out := new(ProjectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectStatus) DeepCopyInto(out *ProjectStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProjectId != nil { + in, out := &in.ProjectId, &out.ProjectId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectStatus. +func (in *ProjectStatus) DeepCopy() *ProjectStatus { + if in == nil { + return nil + } + out := new(ProjectStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resource) DeepCopyInto(out *Resource) { + *out = *in + if in.Organization != nil { + in, out := &in.Organization, &out.Organization + *out = new(OrganizationResource) + **out = **in + } + if in.Connection != nil { + in, out := &in.Connection, &out.Connection + *out = new(ConnectionResource) + **out = **in + } + if in.Project != nil { + in, out := &in.Project, &out.Project + *out = new(ProjectResource) + **out = **in + } + if in.ProjectGrant != nil { + in, out := &in.ProjectGrant, &out.ProjectGrant + *out = new(ProjectGrantResource) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. +func (in *Resource) DeepCopy() *Resource { + if in == nil { + return nil + } + out := new(Resource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Role) DeepCopyInto(out *Role) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Role. +func (in *Role) DeepCopy() *Role { + if in == nil { + return nil + } + out := new(Role) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserPassword) DeepCopyInto(out *UserPassword) { + *out = *in + in.PasswordSecretKey.DeepCopyInto(&out.PasswordSecretKey) + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserPassword. +func (in *UserPassword) DeepCopy() *UserPassword { + if in == nil { + return nil + } + out := new(UserPassword) + in.DeepCopyInto(out) + return out +} diff --git a/src/cmd/main.go b/src/cmd/main.go new file mode 100644 index 0000000..f8de394 --- /dev/null +++ b/src/cmd/main.go @@ -0,0 +1,150 @@ +/* +Copyright 2024. + +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 ( + "flag" + "os" + "time" + + // "time" + + // 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/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + server "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-resources-operator/internal/controller" + "github.com/HaimKortovich/zitadel-resources-operator/pkg/builder" + conditions "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition" + //+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 + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + 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.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: server.Options{BindAddress: metricsAddr}, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "88a0b43c.github.com", + // 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) + } + client := mgr.GetClient() + scheme := mgr.GetScheme() + builder := builder.NewBuilder(scheme) + refResolver := zitadelv1alpha1.NewRefResolver(client) + conditionReady := conditions.NewReady() + requeueZitadel := 5 * time.Minute + + if err = controller.NewConnectionReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Organization") + os.Exit(1) + } + + // if err = controller.NewOrganizationReconciler(client, refResolver, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "Organization") + // os.Exit(1) + // } + + // if err = controller.NewProjectReconciler(client, refResolver, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "Project") + // os.Exit(1) + // } + // if err = controller.NewOIDCAppReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "OIDCApp") + // os.Exit(1) + // } + // if err = controller.NewMachineUserReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "MachineUser") + // os.Exit(1) + // } + // if err = controller.NewAPIAppReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "APIApp") + // os.Exit(1) + // } + // if err = controller.NewActionReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "Action") + // os.Exit(1) + // } + // if err = controller.NewFlowReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "Flow") + // 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/src/config/crd/bases/zitadel.github.com_actions.yaml b/src/config/crd/bases/zitadel.github.com_actions.yaml new file mode 100644 index 0000000..aefe614 --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_actions.yaml @@ -0,0 +1,173 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: actions.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: Action + listKind: ActionList + plural: actions + singular: action + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Action is the Schema for the actions 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: ActionSpec defines the desired state of Action + properties: + allowedToFail: + default: true + type: boolean + organizationRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + script: + type: string + timeout: + format: duration + type: string + required: + - allowedToFail + - organizationRef + - script + - timeout + type: object + status: + description: ActionStatus defines the observed state of Action + properties: + actionId: + default: "" + type: string + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - actionId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_apiapps.yaml b/src/config/crd/bases/zitadel.github.com_apiapps.yaml new file mode 100644 index 0000000..600cfa3 --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_apiapps.yaml @@ -0,0 +1,176 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: apiapps.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: APIApp + listKind: APIAppList + plural: apiapps + singular: apiapp + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: APIApp is the Schema for the apiapps 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: APIAppSpec defines the desired state of APIApp + properties: + authMethodType: + enum: + - API_AUTH_METHOD_TYPE_BASIC + - API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + type: string + projectRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - authMethodType + - projectRef + type: object + status: + description: APIAppStatus defines the observed state of APIApp + properties: + appId: + default: "" + type: string + clientId: + default: "" + type: string + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + keyId: + default: "" + type: string + required: + - appId + - clientId + - keyId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_connections.yaml b/src/config/crd/bases/zitadel.github.com_connections.yaml new file mode 100644 index 0000000..4617d0d --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_connections.yaml @@ -0,0 +1,239 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: connections.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: Connection + listKind: ConnectionList + plural: connections + singular: connection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Connection is the Schema for the connections 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: ConnectionSpec defines the desired state of Connection + properties: + authentication: + properties: + jwt: + properties: + jwtSecretKey: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + scopes: + items: + type: string + type: array + required: + - jwtSecretKey + - scopes + type: object + password: + properties: + passwordSecretKey: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + scopes: + items: + type: string + type: array + username: + type: string + required: + - passwordSecretKey + - scopes + - username + type: object + pat: + description: |- + 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. + properties: + tokenSecretKey: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - tokenSecretKey + type: object + type: object + x-kubernetes-validations: + - message: exactly one of pat, password, or jwt must be specified + rule: '[has(self.pat), has(self.password), has(self.jwt)].filter(x, + x).size() == 1' + host: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + type: string + insecureSkipVerifyTLS: + default: false + type: boolean + port: + type: integer + secure: + default: true + type: boolean + required: + - authentication + - host + - insecureSkipVerifyTLS + - secure + type: object + status: + description: ConnectionStatus defines the observed state of Connection + properties: + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + Conditions for the Connection object. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_flows.yaml b/src/config/crd/bases/zitadel.github.com_flows.yaml new file mode 100644 index 0000000..668c757 --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_flows.yaml @@ -0,0 +1,227 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: flows.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: Flow + listKind: FlowList + plural: flows + singular: flow + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Flow is the Schema for the flows 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: FlowSpec defines the desired state of Flow + properties: + actionRefs: + items: + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: array + flowType: + enum: + - FLOW_TYPE_EXTERNAL_AUTHENTICATION + - "1" + - "2" + - "3" + - "4" + type: string + organizationRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + triggerType: + enum: + - TRIGGER_TYPE_POST_AUTHENTICATION + - TRIGGER_TYPE_PRE_CREATION + - TRIGGER_TYPE_POST_CREATION + - TRIGGER_TYPE_POST_AUTHENTICATION + - TRIGGER_TYPE_PRE_CREATION + - TRIGGER_TYPE_POST_CREATION + - "1" + - "2" + - "3" + - "4" + - "5" + - "6" + type: string + required: + - actionRefs + - flowType + - organizationRef + - triggerType + type: object + status: + description: FlowStatus defines the observed state of Flow + properties: + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_machineusers.yaml b/src/config/crd/bases/zitadel.github.com_machineusers.yaml new file mode 100644 index 0000000..ac92fb4 --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_machineusers.yaml @@ -0,0 +1,278 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: machineusers.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: MachineUser + listKind: MachineUserList + plural: machineusers + singular: machineuser + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MachineUser is the Schema for the machineusers 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: MachineUserSpec defines the desired state of MachineUser + properties: + accessTokenType: + enum: + - ACCESS_TOKEN_TYPE_BEARER + - ACCESS_TOKEN_TYPE_JWT + type: string + authorizations: + items: + properties: + projectRef: + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + roleKeys: + items: + type: string + type: array + required: + - projectRef + type: object + type: array + internalPermissions: + items: + properties: + resource: + properties: + connection: + type: object + organization: + properties: + orgId: + type: string + required: + - orgId + type: object + project: + properties: + projectId: + type: string + required: + - projectId + type: object + projectGrant: + properties: + orgId: + type: string + projectId: + type: string + required: + - orgId + - projectId + type: object + type: object + x-kubernetes-validations: + - message: exactly one of organization, connection, project, + or projectGrant must be specified + rule: '[has(self.organization), has(self.connection), has(self.project), + has(self.projectGrant)].filter(x, x).size() == 1' + roles: + items: + type: string + maxItems: 50 + type: array + required: + - resource + type: object + maxItems: 100 + type: array + metadata: + items: + additionalProperties: + type: string + type: object + type: array + organizationRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + username: + type: string + required: + - accessTokenType + - organizationRef + - username + type: object + status: + description: MachineUserStatus defines the observed state of MachineUser + properties: + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + keyId: + type: string + patId: + type: string + userId: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_oidcapps.yaml b/src/config/crd/bases/zitadel.github.com_oidcapps.yaml new file mode 100644 index 0000000..90b79a2 --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_oidcapps.yaml @@ -0,0 +1,242 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: oidcapps.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: OIDCApp + listKind: OIDCAppList + plural: oidcapps + singular: oidcapp + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: OIDCApp is the Schema for the oidcapps 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: OIDCAppSpec defines the desired state of OIDCApp + properties: + accessTokenRoleAssertion: + type: boolean + accessTokenType: + enum: + - OIDC_TOKEN_TYPE_BEARER + - OIDC_TOKEN_TYPE_JWT + type: string + additionalOrigins: + items: + type: string + type: array + appType: + enum: + - OIDC_APP_TYPE_WEB + - OIDC_APP_TYPE_USER_AGENT + - OIDC_APP_TYPE_NATIVE + type: string + authMethodType: + enum: + - OIDC_AUTH_METHOD_TYPE_BASIC + - OIDC_AUTH_METHOD_TYPE_POST + - OIDC_AUTH_METHOD_TYPE_NONE + - OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + type: string + backChannelLogoutUri: + type: string + clockSkew: + format: duration + type: string + devMode: + type: boolean + grantTypes: + items: + enum: + - OIDC_GRANT_TYPE_AUTHORIZATION_CODE + - OIDC_GRANT_TYPE_IMPLICIT + - OIDC_GRANT_TYPE_REFRESH_TOKEN + - OIDC_GRANT_TYPE_DEVICE_CODE + - OIDC_GRANT_TYPE_TOKEN_EXCHANGE + type: string + type: array + idTokenRoleAssertion: + type: boolean + idTokenUserinfoAssertion: + type: boolean + oidcAppName: + type: string + postLogoutRedirectUris: + items: + type: string + type: array + projectRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + redirectUris: + items: + type: string + type: array + responseTypes: + items: + enum: + - OIDC_RESPONSE_TYPE_CODE + - OIDC_RESPONSE_TYPE_ID_TOKEN + - OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN + type: string + type: array + skipNativeAppSuccessPage: + type: boolean + required: + - accessTokenRoleAssertion + - accessTokenType + - appType + - authMethodType + - clockSkew + - devMode + - grantTypes + - idTokenRoleAssertion + - idTokenUserinfoAssertion + - oidcAppName + - postLogoutRedirectUris + - projectRef + - redirectUris + - responseTypes + - skipNativeAppSuccessPage + type: object + status: + description: OIDCAppStatus defines the observed state of OIDCApp + properties: + appId: + type: string + clientId: + type: string + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - appId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_organizations.yaml b/src/config/crd/bases/zitadel.github.com_organizations.yaml new file mode 100644 index 0000000..1adb5cd --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_organizations.yaml @@ -0,0 +1,163 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: organizations.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: Organization + listKind: OrganizationList + plural: organizations + singular: organization + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Organization is the Schema for the organizations 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: OrganizationSpec defines the desired state of Organization + properties: + connectionRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + organizationName: + type: string + required: + - connectionRef + - organizationName + type: object + status: + description: OrganizationStatus defines the observed state of Organization + properties: + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + Conditions for the Database object. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + organizationId: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.github.com_projects.yaml b/src/config/crd/bases/zitadel.github.com_projects.yaml new file mode 100644 index 0000000..01f1eea --- /dev/null +++ b/src/config/crd/bases/zitadel.github.com_projects.yaml @@ -0,0 +1,239 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: projects.zitadel.github.com +spec: + group: zitadel.github.com + names: + kind: Project + listKind: ProjectList + plural: projects + singular: project + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Project is the Schema for the projects 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: ProjectSpec defines the desired state of Project + properties: + grants: + items: + properties: + organizationRef: + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + roleKeys: + items: + type: string + type: array + required: + - organizationRef + - roleKeys + type: object + type: array + hasProjectCheck: + type: boolean + organizationRef: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + https://zitadel.com/docs/apis/resources/mgmt/management-service-add-project + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + projectName: + type: string + projectRoleAssertion: + type: boolean + projectRoleCheck: + type: boolean + roles: + items: + properties: + displayName: + type: string + group: + type: string + key: + type: string + required: + - displayName + - group + - key + type: object + type: array + required: + - organizationRef + - projectName + type: object + status: + description: ProjectStatus defines the observed state of Project + properties: + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + Conditions for the Database object. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + projectId: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/kustomization.yaml b/src/config/crd/kustomization.yaml new file mode 100644 index 0000000..430dd52 --- /dev/null +++ b/src/config/crd/kustomization.yaml @@ -0,0 +1,43 @@ +# 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/zitadel.github.com_organizations.yaml +- bases/zitadel.github.com_projects.yaml +- bases/zitadel.github.com_oidcapps.yaml +- bases/zitadel.github.com_machineusers.yaml +- bases/zitadel.github.com_apiapps.yaml +- bases/zitadel.github.com_actions.yaml +- bases/zitadel.github.com_flows.yaml +- bases/zitadel.github.com_connections.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_zitadelclusters.yaml +#- patches/webhook_in_organizations.yaml +#- patches/webhook_in_projects.yaml +#- patches/webhook_in_oidcapps.yaml +#- patches/webhook_in_machineusers.yaml +#- patches/webhook_in_apiapps.yaml +#- patches/webhook_in_actions.yaml +#- patches/webhook_in_flows.yaml +#+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 +#- patches/cainjection_in_zitadelclusters.yaml +#- patches/cainjection_in_organizations.yaml +#- patches/cainjection_in_projects.yaml +#- patches/cainjection_in_oidcapps.yaml +#- patches/cainjection_in_machineusers.yaml +#- patches/cainjection_in_apiapps.yaml +#- patches/cainjection_in_actions.yaml +#- patches/cainjection_in_flows.yaml +#- path: patches/cainjection_in_connections.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/src/config/crd/kustomizeconfig.yaml b/src/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..ec5c150 --- /dev/null +++ b/src/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/src/config/default/kustomization.yaml b/src/config/default/kustomization.yaml new file mode 100644 index 0000000..7df9acc --- /dev/null +++ b/src/config/default/kustomization.yaml @@ -0,0 +1,144 @@ +# Adds namespace to all resources. +namespace: src-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: src- + +# 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 + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + + + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- 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 +#- 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/src/config/default/manager_auth_proxy_patch.yaml b/src/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000..b751266 --- /dev/null +++ b/src/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,55 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/src/config/default/manager_config_patch.yaml b/src/config/default/manager_config_patch.yaml new file mode 100644 index 0000000..f6f5891 --- /dev/null +++ b/src/config/default/manager_config_patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/src/config/manager/kustomization.yaml b/src/config/manager/kustomization.yaml new file mode 100644 index 0000000..5c5f0b8 --- /dev/null +++ b/src/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/src/config/manager/manager.yaml b/src/config/manager/manager.yaml new file mode 100644 index 0000000..b57a826 --- /dev/null +++ b/src/config/manager/manager.yaml @@ -0,0 +1,102 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + 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: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + 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 + 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/src/config/rbac/action_editor_role.yaml b/src/config/rbac/action_editor_role.yaml new file mode 100644 index 0000000..d760c36 --- /dev/null +++ b/src/config/rbac/action_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit actions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: action-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: action-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - actions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - actions/status + verbs: + - get diff --git a/src/config/rbac/action_viewer_role.yaml b/src/config/rbac/action_viewer_role.yaml new file mode 100644 index 0000000..0f76dc4 --- /dev/null +++ b/src/config/rbac/action_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view actions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: action-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: action-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - actions + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - actions/status + verbs: + - get diff --git a/src/config/rbac/apiapp_editor_role.yaml b/src/config/rbac/apiapp_editor_role.yaml new file mode 100644 index 0000000..817c204 --- /dev/null +++ b/src/config/rbac/apiapp_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit apiapps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: apiapp-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: apiapp-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - apiapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - apiapps/status + verbs: + - get diff --git a/src/config/rbac/apiapp_viewer_role.yaml b/src/config/rbac/apiapp_viewer_role.yaml new file mode 100644 index 0000000..6ac342b --- /dev/null +++ b/src/config/rbac/apiapp_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view apiapps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: apiapp-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: apiapp-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - apiapps + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - apiapps/status + verbs: + - get diff --git a/src/config/rbac/auth_proxy_client_clusterrole.yaml b/src/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 0000000..e542bd6 --- /dev/null +++ b/src/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/src/config/rbac/auth_proxy_role.yaml b/src/config/rbac/auth_proxy_role.yaml new file mode 100644 index 0000000..5c8d16e --- /dev/null +++ b/src/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,24 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/src/config/rbac/auth_proxy_role_binding.yaml b/src/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 0000000..1d2f986 --- /dev/null +++ b/src/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/src/config/rbac/auth_proxy_service.yaml b/src/config/rbac/auth_proxy_service.yaml new file mode 100644 index 0000000..62b13c0 --- /dev/null +++ b/src/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/src/config/rbac/cluster_editor_role.yaml b/src/config/rbac/cluster_editor_role.yaml new file mode 100644 index 0000000..287dd30 --- /dev/null +++ b/src/config/rbac/cluster_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit zitadelclusters. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: zitadelcluster-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: zitadelcluster-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - clusters/status + verbs: + - get diff --git a/src/config/rbac/connection_editor_role.yaml b/src/config/rbac/connection_editor_role.yaml new file mode 100644 index 0000000..e85e04d --- /dev/null +++ b/src/config/rbac/connection_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit connections. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: src + app.kubernetes.io/managed-by: kustomize + name: connection-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - connections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - connections/status + verbs: + - get diff --git a/src/config/rbac/connection_viewer_role.yaml b/src/config/rbac/connection_viewer_role.yaml new file mode 100644 index 0000000..d9b2a6f --- /dev/null +++ b/src/config/rbac/connection_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view connections. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: src + app.kubernetes.io/managed-by: kustomize + name: connection-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - connections + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - connections/status + verbs: + - get diff --git a/src/config/rbac/flow_editor_role.yaml b/src/config/rbac/flow_editor_role.yaml new file mode 100644 index 0000000..065c47b --- /dev/null +++ b/src/config/rbac/flow_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit flows. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: flow-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: flow-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - flows + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - flows/status + verbs: + - get diff --git a/src/config/rbac/flow_viewer_role.yaml b/src/config/rbac/flow_viewer_role.yaml new file mode 100644 index 0000000..452f5c2 --- /dev/null +++ b/src/config/rbac/flow_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view flows. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: flow-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: flow-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - flows + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - flows/status + verbs: + - get diff --git a/src/config/rbac/kustomization.yaml b/src/config/rbac/kustomization.yaml new file mode 100644 index 0000000..c04d6d3 --- /dev/null +++ b/src/config/rbac/kustomization.yaml @@ -0,0 +1,25 @@ +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 +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.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. +- connection_editor_role.yaml +- connection_viewer_role.yaml + diff --git a/src/config/rbac/leader_election_role.yaml b/src/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..0d272be --- /dev/null +++ b/src/config/rbac/leader_election_role.yaml @@ -0,0 +1,44 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + 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/src/config/rbac/leader_election_role_binding.yaml b/src/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..7d4eb79 --- /dev/null +++ b/src/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + 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/src/config/rbac/machineuser_editor_role.yaml b/src/config/rbac/machineuser_editor_role.yaml new file mode 100644 index 0000000..0db4394 --- /dev/null +++ b/src/config/rbac/machineuser_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit machineusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: machineuser-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: machineuser-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - machineusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - machineusers/status + verbs: + - get diff --git a/src/config/rbac/machineuser_viewer_role.yaml b/src/config/rbac/machineuser_viewer_role.yaml new file mode 100644 index 0000000..d0883d7 --- /dev/null +++ b/src/config/rbac/machineuser_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view machineusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: machineuser-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: machineuser-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - machineusers + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - machineusers/status + verbs: + - get diff --git a/src/config/rbac/oidcapp_editor_role.yaml b/src/config/rbac/oidcapp_editor_role.yaml new file mode 100644 index 0000000..f156264 --- /dev/null +++ b/src/config/rbac/oidcapp_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit oidcapps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: oidcapp-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: oidcapp-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - oidcapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - oidcapps/status + verbs: + - get diff --git a/src/config/rbac/oidcapp_viewer_role.yaml b/src/config/rbac/oidcapp_viewer_role.yaml new file mode 100644 index 0000000..0a0d04a --- /dev/null +++ b/src/config/rbac/oidcapp_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view oidcapps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: oidcapp-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: oidcapp-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - oidcapps + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - oidcapps/status + verbs: + - get diff --git a/src/config/rbac/organization_editor_role.yaml b/src/config/rbac/organization_editor_role.yaml new file mode 100644 index 0000000..eb54313 --- /dev/null +++ b/src/config/rbac/organization_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit organizations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: organization-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: organization-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - organizations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - organizations/status + verbs: + - get diff --git a/src/config/rbac/organization_viewer_role.yaml b/src/config/rbac/organization_viewer_role.yaml new file mode 100644 index 0000000..04b4149 --- /dev/null +++ b/src/config/rbac/organization_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view organizations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: organization-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: organization-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - organizations + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - organizations/status + verbs: + - get diff --git a/src/config/rbac/project_editor_role.yaml b/src/config/rbac/project_editor_role.yaml new file mode 100644 index 0000000..aedada6 --- /dev/null +++ b/src/config/rbac/project_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit projects. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: project-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: project-editor-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - projects + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - projects/status + verbs: + - get diff --git a/src/config/rbac/project_viewer_role.yaml b/src/config/rbac/project_viewer_role.yaml new file mode 100644 index 0000000..74260e6 --- /dev/null +++ b/src/config/rbac/project_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view projects. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: project-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: project-viewer-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - projects + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.github.com + resources: + - projects/status + verbs: + - get diff --git a/src/config/rbac/role.yaml b/src/config/rbac/role.yaml new file mode 100644 index 0000000..db3b9d6 --- /dev/null +++ b/src/config/rbac/role.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - zitadel.github.com + resources: + - connections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.github.com + resources: + - connections/finalizers + verbs: + - update +- apiGroups: + - zitadel.github.com + resources: + - connections/status + verbs: + - get + - patch + - update diff --git a/src/config/rbac/role_binding.yaml b/src/config/rbac/role_binding.yaml new file mode 100644 index 0000000..4d224de --- /dev/null +++ b/src/config/rbac/role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + 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/src/config/rbac/service_account.yaml b/src/config/rbac/service_account.yaml new file mode 100644 index 0000000..a94f6f4 --- /dev/null +++ b/src/config/rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/src/config/samples/kustomization.yaml b/src/config/samples/kustomization.yaml new file mode 100644 index 0000000..3144480 --- /dev/null +++ b/src/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- zitadel_v1alpha1_connection.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/src/config/samples/zitadel_v1alpha1_connection.yaml b/src/config/samples/zitadel_v1alpha1_connection.yaml new file mode 100644 index 0000000..4b3dc58 --- /dev/null +++ b/src/config/samples/zitadel_v1alpha1_connection.yaml @@ -0,0 +1,9 @@ +apiVersion: zitadel.github.com/v1alpha1 +kind: Connection +metadata: + labels: + app.kubernetes.io/name: src + app.kubernetes.io/managed-by: kustomize + name: connection-sample +spec: + # TODO(user): Add fields here diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..e53b530 --- /dev/null +++ b/src/go.mod @@ -0,0 +1,101 @@ +module github.com/HaimKortovich/zitadel-resources-operator + +go 1.25.8 + +require ( + github.com/hashicorp/go-multierror v1.1.1 + github.com/onsi/ginkgo/v2 v2.23.3 + github.com/onsi/gomega v1.36.3 + github.com/sethvargo/go-password v0.3.1 + github.com/zitadel/zitadel-go/v3 v3.27.0 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.11 + k8s.io/api v0.32.3 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v0.32.3 + k8s.io/utils v0.0.0-20241210054802-24370beab758 + sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudnative-pg/barman-cloud v0.1.0 // indirect + github.com/cloudnative-pg/machinery v0.1.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // 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.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 // indirect + github.com/prometheus/client_golang v1.21.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/zitadel/logging v0.7.0 // indirect + github.com/zitadel/oidc/v3 v3.45.5 // indirect + github.com/zitadel/schema v1.3.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.41.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.32.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..fd74911 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,265 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +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/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +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/cloudnative-pg/barman-cloud v0.1.0 h1:e/z52CehMBIh1LjZqNBJnncWJbS+1JYvRMBR8Js6Uiw= +github.com/cloudnative-pg/barman-cloud v0.1.0/go.mod h1:rJUJO/f1yNckLZiVxHAyRmKY+4EPJkYRJsGbTZRJQSY= +github.com/cloudnative-pg/cloudnative-pg v1.25.1 h1:Yc6T7ikQ1AiWXBQht+6C3DoihrIpUN2OkM1dIwqadTo= +github.com/cloudnative-pg/cloudnative-pg v1.25.1/go.mod h1:96b9bRFLSr3uFWHjhytPdcvKIKwy9H6AG7cH0O6jefs= +github.com/cloudnative-pg/machinery v0.1.0 h1:tjRmsqQmsO/OlaT0uFmkEtVqgr+SGPM88cKZOHYKLBo= +github.com/cloudnative-pg/machinery v0.1.0/go.mod h1:0V3vm44FaIsY+x4pm8ORry7xCC3AJiO+ebfPNxeP5Ck= +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.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +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-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +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/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 h1:Q3jQ1NkFqv5o+F8dMmHd8SfEmlcwNeo1immFApntEwE= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +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/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= +github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +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-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 h1:DP+PUNVOc+Bkft8a4QunLzaZ0RspWuD3tBbcPHr2PeE= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1/go.mod h1:6x4x0t9BP35g4XcjkHE9EB3RxhyfxpdpmZKd/Qyk8+M= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +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/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= +github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +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= +github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= +github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= +github.com/zitadel/oidc/v3 v3.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrDQ= +github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE= +github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= +github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= +github.com/zitadel/zitadel-go/v3 v3.27.0 h1:1BumImnIk3D9JYTq+IlVq793vZCXuMZuz1meWzeMQN4= +github.com/zitadel/zitadel-go/v3 v3.27.0/go.mod h1:8UaWIIUR+c9jstT6bjoiknaxttxFZKNc7RYs32v03jw= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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/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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= +k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +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-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= +k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +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/src/hack/boilerplate.go.txt b/src/hack/boilerplate.go.txt new file mode 100644 index 0000000..ff72ff2 --- /dev/null +++ b/src/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2024. + +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. +*/ \ No newline at end of file diff --git a/src/internal/controller/action_controller.gold b/src/internal/controller/action_controller.gold new file mode 100644 index 0000000..e7093d0 --- /dev/null +++ b/src/internal/controller/action_controller.gold @@ -0,0 +1,192 @@ +/* +Copyright 2024. + +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" + "strings" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + "github.com/zitadel/zitadel-go/v3/pkg/client/management" + "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" + pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + durationpb "google.golang.org/protobuf/types/known/durationpb" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ActionReconciler reconciles a Action object +type ActionReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewActionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *ActionReconciler { + return &ActionReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions/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. +func (r *ActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var Action zitadelv1alpha1.Action + if err := r.Get(ctx, req.NamespacedName, &Action); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedActionReconciler(r.Client, r.RefResolver, r.Builder, &Action) + wf := newWrappedActionFinalizer(r.Client, &Action, r.RefResolver) + tf := zitadel.NewZitadelFinalizer(r.Client, wf) + tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &Action) + if err != nil { + return result, fmt.Errorf("error reconciling in ActionReconciler: %v", err) + } + return result, nil +} + +type wrappedActionReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + Action *zitadelv1alpha1.Action + Builder *builder.Builder +} + +func newWrappedActionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + Action *zitadelv1alpha1.Action) zitadel.WrappedReconciler { + return &wrappedActionReconciler{ + Client: client, + refResolver: refResolver, + Action: Action, + Builder: builder, + } +} + +type actionReoncilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + +func (wr *wrappedActionReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []actionReoncilePhase{ + { + Name: "action", + Reconcile: wr.reconcileAction, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedActionReconciler) reconcileAction(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.Action.Spec.OrganizationRef, wr.Action.Namespace) + if err != nil { + return err + } + ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + + if wr.Action.Status.ActionId != "" { + p, err := ztdClient.GetAction(ctx, &pb.GetActionRequest{Id: wr.Action.Status.ActionId}) + if p != nil { + _, err := ztdClient.UpdateAction(ctx, + &pb.UpdateActionRequest{ + Id: p.Action.Id, + Name: wr.Action.Name, + Script: wr.Action.Spec.Script, + Timeout: durationpb.New(wr.Action.Spec.Timeout.Duration), + AllowedToFail: wr.Action.Spec.AllowedToFail, + }, + ) + + if err != nil { + if !strings.Contains(err.Error(), "No changes") { + return fmt.Errorf("Error updating Action: %v", err) + } + } + return nil + } + if err != nil { + if !strings.Contains(err.Error(), "not found") { + return fmt.Errorf("Error getting Action: %v", err) + } + } + } + + resp, err := ztdClient.CreateAction(ctx, + &pb.CreateActionRequest{ + Name: wr.Action.Name, + Script: wr.Action.Spec.Script, + Timeout: durationpb.New(wr.Action.Spec.Timeout.Duration), + AllowedToFail: wr.Action.Spec.AllowedToFail, + }, + ) + + if err != nil { + if strings.Contains(err.Error(), "AlreadyExists") { + return nil + } + return fmt.Errorf("error creating action in Zitadel: %v", err) + } + patch := ctrlClient.MergeFrom(wr.Action.DeepCopy()) + wr.Action.Status.ActionId = resp.Id + return wr.Client.Status().Patch(ctx, wr.Action, patch) +} + +func (wr *wrappedActionReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.Action.DeepCopy()) + patcher(&wr.Action.Status) + + if err := wr.Client.Status().Patch(ctx, wr.Action, patch); err != nil { + return fmt.Errorf("error patching Action status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ActionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Action{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/action_controller_finalizer.gold b/src/internal/controller/action_controller_finalizer.gold new file mode 100644 index 0000000..afff6fc --- /dev/null +++ b/src/internal/controller/action_controller_finalizer.gold @@ -0,0 +1,94 @@ +package controller + +import ( + "strings" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + + "context" + "fmt" + + "github.com/zitadel/zitadel-go/v3/pkg/client/management" + "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" + pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + actionFinalizerName = "action.zitadel.topmanage.com/action" +) + +type wrappedActionFinalizer struct { + client.Client + action *zitadelv1alpha1.Action + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedActionFinalizer(client client.Client, action *zitadelv1alpha1.Action, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer { + return &wrappedActionFinalizer{ + Client: client, + action: action, + refresolver: refresolver, + } +} + +func (wf *wrappedActionFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.action, func(action *zitadelv1alpha1.Action) { + controllerutil.AddFinalizer(action, actionFinalizerName) + }) +} + +func (wf *wrappedActionFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.action, func(action *zitadelv1alpha1.Action) { + controllerutil.RemoveFinalizer(wf.action, actionFinalizerName) + }) +} + +func (wr *wrappedActionFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.action, actionFinalizerName) +} + +func (wf *wrappedActionFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { + if wf.action.Status.ActionId == "" { + return nil + } + org, err := wf.refresolver.OrganizationRef(ctx, &wf.action.Spec.OrganizationRef, wf.action.Namespace) + if err != nil { + return err + } + { + + _, err := ztdClient.GetAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetActionRequest{Id: wf.action.Status.ActionId}) + if err != nil { + if strings.Contains(err.Error(), `not found`) { + return nil + } + return err + } + } + _, err = ztdClient.DeleteAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.DeleteActionRequest{Id: wf.action.Status.ActionId}) + if err != nil { + return err + } + return nil +} + +func (wr *wrappedActionFinalizer) patch(ctx context.Context, action *zitadelv1alpha1.Action, + patchFn func(*zitadelv1alpha1.Action)) error { + patch := ctrlClient.MergeFrom(action.DeepCopy()) + patchFn(action) + + if err := wr.Client.Patch(ctx, action, patch); err != nil { + return fmt.Errorf("error patching Action finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/apiapp_controller.gold b/src/internal/controller/apiapp_controller.gold new file mode 100644 index 0000000..b0ad1ef --- /dev/null +++ b/src/internal/controller/apiapp_controller.gold @@ -0,0 +1,282 @@ +/* +Copyright 2024. + +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" + "encoding/json" + "fmt" + "strings" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + "github.com/zitadel/zitadel-go/v3/pkg/client/management" + "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" + app "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/app" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/authn" + pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// APIAppReconciler reconciles a APIApp object +type APIAppReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *APIAppReconciler { + return &APIAppReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/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. +func (r *APIAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var APIApp zitadelv1alpha1.APIApp + if err := r.Get(ctx, req.NamespacedName, &APIApp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedAPIAppReconciler(r.Client, r.RefResolver, r.Builder, &APIApp) + wf := newWrappedAPIAppFinalizer(r.Client, &APIApp, r.RefResolver) + tf := zitadel.NewZitadelFinalizer(r.Client, wf) + tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &APIApp) + if err != nil { + return result, fmt.Errorf("error reconciling in APIAppReconciler: %v", err) + } + return result, nil +} + +type wrappedAPIAppReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + APIApp *zitadelv1alpha1.APIApp + Builder *builder.Builder +} + +func newWrappedAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + APIApp *zitadelv1alpha1.APIApp) zitadel.WrappedReconciler { + return &wrappedAPIAppReconciler{ + Client: client, + refResolver: refResolver, + APIApp: APIApp, + Builder: builder, + } +} + +type apiAppReoncilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + +func (wr *wrappedAPIAppReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []projectReconcilePhase{ + { + Name: "apiapp", + Reconcile: wr.reconcileApp, + }, + { + Name: "keys", + Reconcile: wr.reconcileKeys, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedAPIAppReconciler) reconcileApp(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.APIApp.Organization(ctx, wr.refResolver) + if err != nil { + return err + } + project, err := wr.APIApp.Project(ctx, wr.refResolver) + if err != nil { + return err + } + if wr.APIApp.Status.AppId != "" { + appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{ + ProjectId: project.Status.ProjectId, + AppId: string(wr.APIApp.Status.AppId), + }) + if err != nil { + return fmt.Errorf("Error getting APIApp: %v", err) + } + if appResp.App != nil { + _, err := ztdClient.UpdateAPIAppConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateAPIAppConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.APIApp.Status.AppId, + AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]), + }) + if err != nil { + if !strings.Contains(err.Error(), "No changes") { + return fmt.Errorf("Error updating APIApp: %v", err) + } + } + return nil + } + } + + resp, err := ztdClient.AddAPIApp(middleware.SetOrgID(ctx, org.Status.OrgId), + &pb.AddAPIAppRequest{ + Name: wr.APIApp.Name, + ProjectId: project.Status.ProjectId, + AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]), + }, + ) + if err != nil { + if strings.Contains(err.Error(), "AlreadyExists") { + return nil + } + return fmt.Errorf("error creating APIApp in Zitadel: %v", err) + } + key := types.NamespacedName{ + Name: wr.APIApp.Name + "-client-secret", + Namespace: wr.APIApp.Namespace, + } + + secretData := map[string][]byte{"client-secret": []byte(resp.ClientSecret)} + secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp) + if err != nil { + return fmt.Errorf("error building Secret: %v", err) + } + if err := wr.Create(ctx, secret); err != nil { + return fmt.Errorf("error creating Client-secret Secret: %v", err) + } + patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy()) + wr.APIApp.Status.AppId = resp.AppId + wr.APIApp.Status.ClientId = resp.ClientId + return wr.Client.Status().Patch(ctx, wr.APIApp, patch) +} + +type Key struct { + Type string `json:"type"` + KeyID string `json:"keyId"` + Key string `json:"key"` + AppID string `json:"appId"` + ClientID string `json:"clientId"` +} + +func (wr *wrappedAPIAppReconciler) reconcileKeys(ctx context.Context, ztdClient *management.Client) error { + if wr.APIApp.Spec.AuthMethodType == "API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT" { + org, err := wr.APIApp.Organization(ctx, wr.refResolver) + if err != nil { + return err + } + project, err := wr.APIApp.Project(ctx, wr.refResolver) + if err != nil { + return err + } + if wr.APIApp.Status.KeyId != "" { + appKey, err := ztdClient.GetAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppKeyRequest{ + ProjectId: project.Status.ProjectId, + AppId: wr.APIApp.Status.AppId, + KeyId: wr.APIApp.Status.KeyId, + }) + if err != nil { + if !strings.Contains(err.Error(), "not found") { + return fmt.Errorf("Could not get key: %v", err) + } + } + if appKey.Key != nil { + return nil + } + } + resp, err := ztdClient.AddAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.AddAppKeyRequest{ + ProjectId: project.Status.ProjectId, + AppId: wr.APIApp.Status.AppId, + Type: authn.KeyType_KEY_TYPE_JSON, + ExpirationDate: nil, + }) + + if err != nil { + return fmt.Errorf("Error adding Key to app: %v", err) + } + + key := types.NamespacedName{ + Name: wr.APIApp.Name + "-privatekey-secret", + Namespace: wr.APIApp.Namespace, + } + var jsonKey Key + if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil { + return fmt.Errorf("Could not unmarshal key details: %v", err) + } + secretData := map[string][]byte{ + "clientId": []byte(jsonKey.ClientID), + "type": []byte(jsonKey.Type), + "keyId": []byte(jsonKey.KeyID), + "appId": []byte(jsonKey.AppID), + "key": []byte(jsonKey.Key), + } + secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp) + if err != nil { + return fmt.Errorf("error building Secret: %v", err) + } + if err := wr.Create(ctx, secret); err != nil { + return fmt.Errorf("error creating private-key Secret: %v", err) + } + patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy()) + wr.APIApp.Status.KeyId = resp.Id + return wr.Client.Status().Patch(ctx, wr.APIApp, patch) + } + return nil +} + +func (wr *wrappedAPIAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.APIApp.DeepCopy()) + patcher(&wr.APIApp.Status) + + if err := wr.Client.Status().Patch(ctx, wr.APIApp, patch); err != nil { + return fmt.Errorf("error patching APIApp status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *APIAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.APIApp{}). + Owns(&corev1.Secret{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/apiapp_controller_finalizer.gold b/src/internal/controller/apiapp_controller_finalizer.gold new file mode 100644 index 0000000..88ca265 --- /dev/null +++ b/src/internal/controller/apiapp_controller_finalizer.gold @@ -0,0 +1,91 @@ +package controller + +import ( + "strings" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + + "context" + "fmt" + + "github.com/zitadel/zitadel-go/v3/pkg/client/management" + "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" + pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + APIAppFinalizerName = "apiapp.zitadel.topmanage.com/apiapp" +) + +type wrappedAPIAppFinalizer struct { + client.Client + APIApp *zitadelv1alpha1.APIApp + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedAPIAppFinalizer(client client.Client, APIApp *zitadelv1alpha1.APIApp, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer { + return &wrappedAPIAppFinalizer{ + Client: client, + APIApp: APIApp, + refresolver: refresolver, + } +} + +func (wf *wrappedAPIAppFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.APIApp, func(APIApp *zitadelv1alpha1.APIApp) { + controllerutil.AddFinalizer(APIApp, APIAppFinalizerName) + }) +} + +func (wf *wrappedAPIAppFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.APIApp, func(APIApp *zitadelv1alpha1.APIApp) { + controllerutil.RemoveFinalizer(wf.APIApp, APIAppFinalizerName) + }) +} + +func (wr *wrappedAPIAppFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.APIApp, APIAppFinalizerName) +} + +func (wf *wrappedAPIAppFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { + if wf.APIApp.Status.AppId == "" { + return nil + } + org, err := wf.APIApp.Organization(ctx, wf.refresolver) + if err != nil { + return err + } + project, err := wf.APIApp.Project(ctx, wf.refresolver) + if err != nil { + return err + } + _, err = ztdClient.RemoveApp(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.RemoveAppRequest{ProjectId: project.Status.ProjectId, AppId: wf.APIApp.Status.AppId}) + if err != nil { + if strings.Contains(err.Error(), "doesn't exist") { + return nil + } + return err + } + return nil +} + +func (wr *wrappedAPIAppFinalizer) patch(ctx context.Context, APIApp *zitadelv1alpha1.APIApp, + patchFn func(*zitadelv1alpha1.APIApp)) error { + patch := ctrlClient.MergeFrom(APIApp.DeepCopy()) + patchFn(APIApp) + + if err := wr.Client.Patch(ctx, APIApp, patch); err != nil { + return fmt.Errorf("error patching APIApp finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/connection_controller.go b/src/internal/controller/connection_controller.go new file mode 100644 index 0000000..a80b238 --- /dev/null +++ b/src/internal/controller/connection_controller.go @@ -0,0 +1,116 @@ +/* +Copyright 2024. + +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" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-resources-operator/pkg/builder" + condition "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-resources-operator/pkg/controller/core" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ConnectionReconciler reconciles a Connection object +type ConnectionReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewConnectionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *ConnectionReconciler { + return &ConnectionReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.github.com,resources=connections,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.github.com,resources=connections/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.github.com,resources=connections/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. +func (r *ConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var Connection zitadelv1alpha1.Connection + if err := r.Get(ctx, req.NamespacedName, &Connection); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedConnectionReconciler(r.Client, r.RefResolver, r.Builder, &Connection) + wf := newWrappedConnectionFinalizer(r.Client, &Connection, r.RefResolver) + tf := core.NewCoreFinalizer(r.Client, wf) + tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &Connection) + if err != nil { + return result, fmt.Errorf("error reconciling in ConnectionReconciler: %v", err) + } + return result, nil +} + +type wrappedConnectionReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + Connection *zitadelv1alpha1.Connection + Builder *builder.Builder +} + +func newWrappedConnectionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + Connection *zitadelv1alpha1.Connection) core.WrappedCoreReconciler { + return &wrappedConnectionReconciler{ + Client: client, + refResolver: refResolver, + Connection: Connection, + Builder: builder, + } +} + +func (wr *wrappedConnectionReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + return nil +} + +func (wr *wrappedConnectionReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.Connection.DeepCopy()) + patcher(&wr.Connection.Status) + + if err := wr.Client.Status().Patch(ctx, wr.Connection, patch); err != nil { + return fmt.Errorf("error patching Connection status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Connection{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/connection_controller_finalizer.go b/src/internal/controller/connection_controller_finalizer.go new file mode 100644 index 0000000..ada388d --- /dev/null +++ b/src/internal/controller/connection_controller_finalizer.go @@ -0,0 +1,69 @@ +package controller + +import ( + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-resources-operator/pkg/controller/core" + + "context" + "fmt" + + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + ConnectionFinalizerName = "connection.zitadel.github.com/connection" +) + +type wrappedConnectionFinalizer struct { + client.Client + Connection *zitadelv1alpha1.Connection + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedConnectionFinalizer(client client.Client, Connection *zitadelv1alpha1.Connection, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer { + return &wrappedConnectionFinalizer{ + Client: client, + Connection: Connection, + refresolver: refresolver, + } +} + +func (wf *wrappedConnectionFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.Connection, func(Connection *zitadelv1alpha1.Connection) { + controllerutil.AddFinalizer(Connection, ConnectionFinalizerName) + }) +} + +func (wf *wrappedConnectionFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.Connection, func(Connection *zitadelv1alpha1.Connection) { + controllerutil.RemoveFinalizer(wf.Connection, ConnectionFinalizerName) + }) +} + +func (wr *wrappedConnectionFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.Connection, ConnectionFinalizerName) +} + +func (wf *wrappedConnectionFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + return nil +} + +func (wr *wrappedConnectionFinalizer) patch(ctx context.Context, Connection *zitadelv1alpha1.Connection, + patchFn func(*zitadelv1alpha1.Connection)) error { + patch := ctrlClient.MergeFrom(Connection.DeepCopy()) + patchFn(Connection) + + if err := wr.Client.Patch(ctx, Connection, patch); err != nil { + return fmt.Errorf("error patching Connection finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/flow_controller.gold b/src/internal/controller/flow_controller.gold new file mode 100644 index 0000000..a91fd40 --- /dev/null +++ b/src/internal/controller/flow_controller.gold @@ -0,0 +1,168 @@ +/* +Copyright 2024. + +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" + "strings" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + "github.com/zitadel/zitadel-go/v3/pkg/client/management" + "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" + pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// FlowReconciler reconciles a Flow object +type FlowReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewFlowReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *FlowReconciler { + return &FlowReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows/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. +func (r *FlowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var Flow zitadelv1alpha1.Flow + if err := r.Get(ctx, req.NamespacedName, &Flow); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedFlowReconciler(r.Client, r.RefResolver, r.Builder, &Flow) + wf := newWrappedFlowFinalizer(r.Client, &Flow, r.RefResolver) + tf := zitadel.NewZitadelFinalizer(r.Client, wf) + tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &Flow) + if err != nil { + return result, fmt.Errorf("error reconciling in FlowReconciler: %v", err) + } + return result, nil +} + +type wrappedFlowReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + Flow *zitadelv1alpha1.Flow + Builder *builder.Builder +} + +func newWrappedFlowReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + Flow *zitadelv1alpha1.Flow) zitadel.WrappedReconciler { + return &wrappedFlowReconciler{ + Client: client, + refResolver: refResolver, + Flow: Flow, + Builder: builder, + } +} + +type flowReoncilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + +func (wr *wrappedFlowReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []flowReoncilePhase{ + { + Name: "flow", + Reconcile: wr.reconcileFlow, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedFlowReconciler) reconcileFlow(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.Flow.Spec.OrganizationRef, wr.Flow.Namespace) + if err != nil { + return err + } + ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + + actionIds := []string{} + for _, actionRef := range wr.Flow.Spec.ActionRefs { + action, err := wr.refResolver.ActionRef(ctx, &actionRef, wr.Flow.Namespace) + if err != nil { + return fmt.Errorf("Error resolving action reference: %v", err) + } + if action.Status.ActionId == "" { + return fmt.Errorf("Action with name: %s not ready for trigger", action.Name) + } + actionIds = append(actionIds, action.Status.ActionId) + } + + _, err = ztdClient.SetTriggerActions(ctx, &pb.SetTriggerActionsRequest{ + FlowType: wr.Flow.Spec.FlowType, + TriggerType: wr.Flow.Spec.TriggerType, + ActionIds: actionIds, + }) + if err != nil { + if !strings.Contains(err.Error(), "No Changes") { + return fmt.Errorf("Error triggering action flow: %v", err) + } + } + return nil +} + +func (wr *wrappedFlowReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.Flow.DeepCopy()) + patcher(&wr.Flow.Status) + + if err := wr.Client.Status().Patch(ctx, wr.Flow, patch); err != nil { + return fmt.Errorf("error patching Flow status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FlowReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Flow{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/flow_controller_finalizer.gold b/src/internal/controller/flow_controller_finalizer.gold new file mode 100644 index 0000000..2823d06 --- /dev/null +++ b/src/internal/controller/flow_controller_finalizer.gold @@ -0,0 +1,82 @@ +package controller + +import ( + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + + "context" + "fmt" + + "github.com/zitadel/zitadel-go/v3/pkg/client/management" + "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" + pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + flowFinalizerName = "flow.zitadel.topmanage.com/flow" +) + +type wrappedFlowFinalizer struct { + client.Client + flow *zitadelv1alpha1.Flow + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedFlowFinalizer(client client.Client, flow *zitadelv1alpha1.Flow, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer { + return &wrappedFlowFinalizer{ + Client: client, + flow: flow, + refresolver: refresolver, + } +} + +func (wf *wrappedFlowFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.flow, func(flow *zitadelv1alpha1.Flow) { + controllerutil.AddFinalizer(flow, flowFinalizerName) + }) +} + +func (wf *wrappedFlowFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.flow, func(flow *zitadelv1alpha1.Flow) { + controllerutil.RemoveFinalizer(wf.flow, flowFinalizerName) + }) +} + +func (wr *wrappedFlowFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.flow, flowFinalizerName) +} + +func (wf *wrappedFlowFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { + org, err := wf.refresolver.OrganizationRef(ctx, &wf.flow.Spec.OrganizationRef, wf.flow.Namespace) + if err != nil { + return err + } + + _, err = ztdClient.ClearFlow(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.ClearFlowRequest{ + Type: wf.flow.Spec.FlowType, + }) + if err != nil { + return err + } + return nil +} + +func (wr *wrappedFlowFinalizer) patch(ctx context.Context, flow *zitadelv1alpha1.Flow, + patchFn func(*zitadelv1alpha1.Flow)) error { + patch := ctrlClient.MergeFrom(flow.DeepCopy()) + patchFn(flow) + + if err := wr.Client.Patch(ctx, flow, patch); err != nil { + return fmt.Errorf("error patching Flow finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/instance_controller_test.gold b/src/internal/controller/instance_controller_test.gold new file mode 100644 index 0000000..0ab5c63 --- /dev/null +++ b/src/internal/controller/instance_controller_test.gold @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +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" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" +) + +var _ = Describe("ZitadelInstance 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 + } + zitadelinstance := &zitadelv1alpha1.ZitadelInstance{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ZitadelInstance") + err := k8sClient.Get(ctx, typeNamespacedName, zitadelinstance) + if err != nil && errors.IsNotFound(err) { + resource := &zitadelv1alpha1.ZitadelInstance{ + 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 := &zitadelv1alpha1.ZitadelInstance{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ZitadelInstance") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ZitadelInstanceReconciler{ + 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/src/internal/controller/machineuser_controller.gold b/src/internal/controller/machineuser_controller.gold new file mode 100644 index 0000000..2f5c82b --- /dev/null +++ b/src/internal/controller/machineuser_controller.gold @@ -0,0 +1,417 @@ +package controller + +import ( + "context" + "fmt" + "slices" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/internal_permission/v2" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/object/v2" + "google.golang.org/protobuf/types/known/timestamppb" + + user "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// MachineUserReconciler reconciles a MachineUser object +type MachineUserReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *MachineUserReconciler { + return &MachineUserReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers/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. +func (r *MachineUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var MachineUser zitadelv1alpha1.MachineUser + if err := r.Get(ctx, req.NamespacedName, &MachineUser); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedMachineUserReconciler(r.Client, r.RefResolver, r.Builder, &MachineUser) + wf := newWrappedMachineUserFinalizer(r.Client, &MachineUser) + tf := core.NewCoreFinalizer(r.Client, wf) + tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &MachineUser) + if err != nil { + return result, fmt.Errorf("error reconciling in MachineUserReconciler: %v", err) + } + return result, nil +} + +type wrappedMachineUserReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + MachineUser *zitadelv1alpha1.MachineUser + Builder *builder.Builder +} + +func newWrappedMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + MachineUser *zitadelv1alpha1.MachineUser) core.WrappedCoreReconciler { + return &wrappedMachineUserReconciler{ + Client: client, + refResolver: refResolver, + MachineUser: MachineUser, + Builder: builder, + } +} + +type machineUserReconcilePhase struct { + Name string + Reconcile func(context.Context, *clientv2.Client) error +} + +func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + phases := []machineUserReconcilePhase{ + { + Name: "machineUser", + Reconcile: wr.reconcileMachineUser, + }, + { + Name: "internalPermissions", + Reconcile: wr.reconcileInternalPermissions, + }, + { + Name: "pat", + Reconcile: wr.reconcilePAT, + }, + // { + // Name: "jwt", + // Reconcile: wr.reconcileJWT, + // }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedMachineUserReconciler) reconcileMachineUser(ctx context.Context, ztdClient *clientv2.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace) + if err != nil { + return err + } + if org.Status.OrganizationId == nil { + return fmt.Errorf("Organization not created yet") + } + + var userId *string + userList, err := ztdClient.UserServiceV2().ListUsers(ctx, &user.ListUsersRequest{ + Queries: []*user.SearchQuery{{ + Query: &user.SearchQuery_AndQuery{ + AndQuery: &user.AndQuery{ + Queries: []*user.SearchQuery{ + &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: wr.MachineUser.Spec.Username, + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + &user.SearchQuery{ + Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: *org.Status.OrganizationId, + }, + }, + }, + }, + }, + }, + }}}) + if err != nil { + return fmt.Errorf("error listing users: %v", err) + } + if len(userList.Result) > 0 { + userId = &userList.Result[0].UserId + } + + if userId == nil { + accesTokenType := user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER + switch wr.MachineUser.Spec.AccessTokenType { + case "ACCESS_TOKEN_TYPE_BEARER": + accesTokenType = user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER + case "ACCESS_TOKEN_TYPE_JWT": + accesTokenType = user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT + } + ztdClient.UserServiceV2().CreateUser(ctx, &user.CreateUserRequest{ + OrganizationId: *org.Status.OrganizationId, + Username: &wr.MachineUser.Spec.Username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: wr.MachineUser.Spec.Username, + AccessTokenType: accesTokenType, + }, + }, + }) + } + + patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) + wr.MachineUser.Status.UserId = userId + return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) +} + +func (wr *wrappedMachineUserReconciler) reconcilePAT(ctx context.Context, ztdClient *clientv2.Client) error { + pats, err := ztdClient.UserServiceV2().ListPersonalAccessTokens(ctx, &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + { + Filter: &user.PersonalAccessTokensSearchFilter_UserIdFilter{ + UserIdFilter: &filter.IDFilter{ + Id: *wr.MachineUser.Status.UserId, + }, + }, + }, + }}) + if err != nil { + return fmt.Errorf("Error getting PAT: %v", err) + } + + if pats.Result == nil || !wr.MachineUser.Status.GetConditionStatus(zitadelv1alpha1.ConditionTypePATUpToDate) { + resp, err := ztdClient.UserServiceV2().AddPersonalAccessToken(ctx, &user.AddPersonalAccessTokenRequest{ + UserId: *wr.MachineUser.Status.UserId, + ExpirationDate: timestamppb.New(time.Now().AddDate(999, 1, 1)), + }) + if err != nil { + return fmt.Errorf("Error adding PAT: %v", err) + } + key := types.NamespacedName{ + Name: wr.MachineUser.PatSecretName(), + Namespace: wr.MachineUser.Namespace, + } + desiredPatSecret, err := wr.Builder.BuildSecret(builder.SecretOpts{ + Key: key, + Immutable: false, + Data: map[string][]byte{ + "pat": []byte(resp.Token), + }, + }, wr.MachineUser) + + if err != nil { + return fmt.Errorf("error building PAT Secret: %v", err) + } + + { + var existingPatSecret corev1.Secret + if err := wr.Get(ctx, key, &existingPatSecret); err != nil { + if errors.IsNotFound(err) { + if err := wr.Create(ctx, desiredPatSecret); err != nil { + return fmt.Errorf("error creating PAT Secret: %v", err) + } + } else { + return fmt.Errorf("error getting PAT Secret: %v", err) + } + } else { + patch := client.MergeFrom(existingPatSecret.DeepCopy()) + existingPatSecret.Data = desiredPatSecret.Data + if err = wr.Patch(ctx, &existingPatSecret, patch); err != nil { + return err + } + } + + } + + if err = wr.PatchStatus(ctx, condition.SetPatUpToDate); err != nil { + return err + } + + patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) + wr.MachineUser.Status.PATId = &resp.TokenId + return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) + } + return nil +} + +// func (wr *wrappedMachineUserReconciler) reconcileJWT(ctx context.Context, ztdClient *management.Client) error { +// org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace) +// if err != nil { +// return err +// } +// ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + +// token, err := ztdClient.GetMachineKeyByIDs(ctx, &pb.GetMachineKeyByIDsRequest{ +// UserId: wr.MachineUser.Status.UserId, +// KeyId: wr.MachineUser.Status.KeyId, +// }) +// if err != nil { +// if !(strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "length must be between 1 and 200 runes")) { +// return fmt.Errorf("Error getting JWT: %v", err) +// } +// } + +// if token == nil { +// resp, err := ztdClient.AddMachineKey(ctx, &pb.AddMachineKeyRequest{ +// UserId: wr.MachineUser.Status.UserId, +// Type: authn.KeyType_KEY_TYPE_JSON, +// }) +// if err != nil { +// return fmt.Errorf("Error adding JWT: %v", err) +// } +// key := types.NamespacedName{ +// Name: wr.MachineUser.JWTSecretName(), +// Namespace: wr.MachineUser.Namespace, +// } + +// var jsonKey Key +// if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil { +// return fmt.Errorf("Could not unmarshal key details: %v", err) +// } + +// secretData := map[string][]byte{ +// "clientId": []byte(jsonKey.ClientID), +// "type": []byte(jsonKey.Type), +// "keyId": []byte(jsonKey.KeyID), +// "appId": []byte(jsonKey.AppID), +// "key": []byte(jsonKey.Key), +// } +// jwtSecret, err := wr.Builder.BuildSecret(builder.SecretOpts{ +// Key: key, +// Immutable: false, +// Data: secretData, +// }, wr.MachineUser) + +// if err != nil { +// return fmt.Errorf("error building machine key Secret: %v", err) +// } +// if err := wr.Create(ctx, jwtSecret); err != nil { +// return fmt.Errorf("error creating machine key Secret: %v", err) +// } +// patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) +// wr.MachineUser.Status.KeyId = resp.KeyId +// return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) +// } +// return nil +// } + +func (wr *wrappedMachineUserReconciler) reconcileInternalPermissions(ctx context.Context, ztdClient *clientv2.Client) error { + for _, permission := range wr.MachineUser.Spec.InternalPermissions { + permissionPbFilter := + &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{}, + } + permissionPb := &internal_permission.ResourceType{} + if permission.Resource.Instance != nil { + permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_Instance{Instance: true} + permissionPb.Resource = &internal_permission.ResourceType_Instance{Instance: true} + } else if permission.Resource.Organization != nil { + permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_OrganizationId{OrganizationId: permission.Resource.Organization.OrgID} + permissionPb.Resource = &internal_permission.ResourceType_OrganizationId{OrganizationId: permission.Resource.Organization.OrgID} + } else if permission.Resource.ProjectGrant != nil { + permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_ProjectGrant_{ProjectGrant: &internal_permission.ResourceFilter_ProjectGrant{ + ProjectId: permission.Resource.ProjectGrant.ProjectID, OrganizationId: permission.Resource.ProjectGrant.ProjectID, + }} + permissionPb.Resource = &internal_permission.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: permission.Resource.ProjectGrant.ProjectID, OrganizationId: permission.Resource.ProjectGrant.ProjectID, + }} + } else if permission.Resource.Project != nil { + permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_ProjectId{ProjectId: permission.Resource.Project.ProjectID} + permissionPb.Resource = &internal_permission.ResourceType_ProjectId{ProjectId: permission.Resource.Project.ProjectID} + } + + var admin *internal_permission.Administrator + adminRoleList, err := ztdClient.InternalPermissionServiceV2().ListAdministrators(ctx, &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: permissionPbFilter, + }, + { + Filter: &internal_permission.AdministratorSearchFilter_And{And: &internal_permission.AndFilter{Queries: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{ + *wr.MachineUser.Status.UserId, + }, + }, + }, + }}, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("error listing admin list: %v", err) + } + if len(adminRoleList.Administrators) > 0 { + admin = adminRoleList.Administrators[0] + } + if admin == nil { + _, err := ztdClient.InternalPermissionServiceV2().CreateAdministrator(ctx, &internal_permission.CreateAdministratorRequest{ + UserId: *wr.MachineUser.Status.UserId, + Roles: permission.Roles, + Resource: permissionPb, + }) + if err != nil { + return fmt.Errorf("error creating admin: %v", err) + } + } else { + uniqueRoles := permission.Roles + uniqueRoles = append(uniqueRoles, admin.Roles...) + uniqueRoles = slices.Compact(uniqueRoles) + _, err := ztdClient.InternalPermissionServiceV2().UpdateAdministrator(ctx, &internal_permission.UpdateAdministratorRequest{ + UserId: *wr.MachineUser.Status.UserId, + Roles: uniqueRoles, + Resource: permissionPb, + }) + if err != nil { + return fmt.Errorf("error updating admin: %v", err) + } + } + } + return nil +} + +func (wr *wrappedMachineUserReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.MachineUser.DeepCopy()) + patcher(&wr.MachineUser.Status) + + if err := wr.Client.Status().Patch(ctx, wr.MachineUser, patch); err != nil { + return fmt.Errorf("error patching MachineUser status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MachineUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.MachineUser{}). + Owns(&corev1.Secret{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/machineuser_controller_finalizer.gold b/src/internal/controller/machineuser_controller_finalizer.gold new file mode 100644 index 0000000..43c445b --- /dev/null +++ b/src/internal/controller/machineuser_controller_finalizer.gold @@ -0,0 +1,78 @@ +package controller + +import ( + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + + "context" + "fmt" + + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2" + + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + machineuserFinalizerName = "machineuser.zitadel.topmanage.com/machineuser" +) + +type wrappedMachineUserFinalizer struct { + client.Client + machineuser *zitadelv1alpha1.MachineUser +} + +func newWrappedMachineUserFinalizer(client client.Client, machineuser *zitadelv1alpha1.MachineUser) core.WrappedCoreFinalizer { + return &wrappedMachineUserFinalizer{ + Client: client, + machineuser: machineuser, + } +} + +func (wf *wrappedMachineUserFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.machineuser, func(machineuser *zitadelv1alpha1.MachineUser) { + controllerutil.AddFinalizer(machineuser, machineuserFinalizerName) + }) +} + +func (wf *wrappedMachineUserFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.machineuser, func(machineuser *zitadelv1alpha1.MachineUser) { + controllerutil.RemoveFinalizer(wf.machineuser, machineuserFinalizerName) + }) +} + +func (wr *wrappedMachineUserFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.machineuser, machineuserFinalizerName) +} + +func (wf *wrappedMachineUserFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + if wf.machineuser.Status.UserId != nil { + if _, err := ztdClient.UserServiceV2().DeleteUser(ctx, + &user.DeleteUserRequest{ + UserId: *wf.machineuser.Status.UserId, + }, + ); err != nil { + return fmt.Errorf("Error deleting organization: %v", err) + } + } + return nil +} + +func (wr *wrappedMachineUserFinalizer) patch(ctx context.Context, machineuser *zitadelv1alpha1.MachineUser, + patchFn func(*zitadelv1alpha1.MachineUser)) error { + patch := ctrlClient.MergeFrom(machineuser.DeepCopy()) + patchFn(machineuser) + + if err := wr.Client.Patch(ctx, machineuser, patch); err != nil { + return fmt.Errorf("error patching MachineUser finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/oidcapp_controller.gold b/src/internal/controller/oidcapp_controller.gold new file mode 100644 index 0000000..6976eec --- /dev/null +++ b/src/internal/controller/oidcapp_controller.gold @@ -0,0 +1,263 @@ +/* +Copyright 2024. + +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" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/application/v2" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2" + durationpb "google.golang.org/protobuf/types/known/durationpb" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// OIDCAppReconciler reconciles a OIDCApp object +type OIDCAppReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewOIDCAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *OIDCAppReconciler { + return &OIDCAppReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps/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. +func (r *OIDCAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var OIDCApp zitadelv1alpha1.OIDCApp + if err := r.Get(ctx, req.NamespacedName, &OIDCApp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedOIDCAppReconciler(r.Client, r.RefResolver, r.Builder, &OIDCApp) + wf := newWrappedOIDCAppFinalizer(r.Client, &OIDCApp, r.RefResolver) + tf := core.NewCoreFinalizer(r.Client, wf) + tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &OIDCApp) + if err != nil { + return result, fmt.Errorf("error reconciling in OIDCAppReconciler: %v", err) + } + return result, nil +} + +type wrappedOIDCAppReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + OIDCApp *zitadelv1alpha1.OIDCApp + Builder *builder.Builder +} + +func newWrappedOIDCAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + OIDCApp *zitadelv1alpha1.OIDCApp) core.WrappedCoreReconciler { + return &wrappedOIDCAppReconciler{ + Client: client, + refResolver: refResolver, + OIDCApp: OIDCApp, + Builder: builder, + } +} + +func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + project, err := wr.OIDCApp.Project(ctx, wr.refResolver) + if err != nil { + return err + } + if project.Status.ProjectId == nil { + return fmt.Errorf("Project has not been created yet...") + } + responseTypes := []application.OIDCResponseType{} + for _, r := range wr.OIDCApp.Spec.ResponseTypes { + responseTypes = append(responseTypes, application.OIDCResponseType(application.OIDCApplicationType_value[string(r)])) + } + grantTypes := []application.OIDCGrantType{} + for _, r := range wr.OIDCApp.Spec.GrantTypes { + grantTypes = append(grantTypes, application.OIDCGrantType(application.OIDCGrantType_value[string(r)])) + } + + var appid *string + var clientid *string + appList, err := ztdClient.ApplicationServiceV2().ListApplications(ctx, + &application.ListApplicationsRequest{ + Filters: []*application.ApplicationSearchFilter{ + { + Filter: &application.ApplicationSearchFilter_NameFilter{ + NameFilter: &application.ApplicationNameFilter{ + Name: wr.OIDCApp.Spec.OIDCAppName, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + { + Filter: &application.ApplicationSearchFilter_ProjectIdFilter{ + ProjectIdFilter: &application.ProjectIDFilter{ + ProjectId: *project.Status.ProjectId, + }, + }, + }, + }, + }, + ) + if err != nil { + return fmt.Errorf("Error listing OIDCApps: %v", err) + } + + if len(appList.Applications) > 0 { + appid = &appList.Applications[0].ApplicationId + clientid = &appList.Applications[0].GetApiConfiguration().ClientId + } + + if appid == nil { + resp, err := ztdClient.ApplicationServiceV2().CreateApplication(ctx, + &application.CreateApplicationRequest{ + Name: wr.OIDCApp.Name, + ProjectId: *project.Status.ProjectId, + ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{ + OidcConfiguration: &application.CreateOIDCApplicationRequest{ + RedirectUris: wr.OIDCApp.Spec.RedirectUris, + ResponseTypes: responseTypes, + GrantTypes: grantTypes, + AuthMethodType: application.OIDCAuthMethodType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AuthMethodType]), + PostLogoutRedirectUris: wr.OIDCApp.Spec.PostLogoutRedirectUris, + Version: application.OIDCVersion_OIDC_VERSION_1_0, + DevelopmentMode: wr.OIDCApp.Spec.DevMode, + AccessTokenType: application.OIDCTokenType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AccessTokenType]), + AccessTokenRoleAssertion: wr.OIDCApp.Spec.AccessTokenRoleAssertion, + IdTokenRoleAssertion: wr.OIDCApp.Spec.IdTokenRoleAssertion, + IdTokenUserinfoAssertion: wr.OIDCApp.Spec.IdTokenUserinfoAssertion, + ClockSkew: durationpb.New(wr.OIDCApp.Spec.ClockSkew.Duration), + AdditionalOrigins: wr.OIDCApp.Spec.AdditionalOrigins, + SkipNativeAppSuccessPage: wr.OIDCApp.Spec.SkipNativeAppSuccessPage, + LoginVersion: &application.LoginVersion{ + Version: &application.LoginVersion_LoginV2{ + LoginV2: &application.LoginV2{ + BaseUri: nil, + }, + }, + }, + BackChannelLogoutUri: wr.OIDCApp.Spec.BackChannelLogoutUri, + }, + }, + }, + ) + if err != nil { + return fmt.Errorf("error creating OIDCApp in Zitadel: %v", err) + } + key := types.NamespacedName{ + Name: wr.OIDCApp.ClientSecretName(), + Namespace: wr.OIDCApp.Namespace, + } + + secretData := map[string][]byte{"clientSecret": []byte(resp.GetApiConfiguration().ClientSecret), "appId": []byte(resp.ApplicationId), "clientId": []byte(resp.GetApiConfiguration().ClientId)} + secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.OIDCApp) + if err != nil { + return fmt.Errorf("error building Secret: %v", err) + } + if err := wr.Create(ctx, secret); err != nil { + return fmt.Errorf("error creating Client-secret Secret: %v", err) + } + appid = &resp.ApplicationId + clientid = &resp.GetApiConfiguration().ClientId + } else { + _, err := ztdClient.ApplicationServiceV2().UpdateApplication(ctx, + &application.UpdateApplicationRequest{ + Name: wr.OIDCApp.Name, + ProjectId: *project.Status.ProjectId, + ApplicationType: &application.UpdateApplicationRequest_OidcConfiguration{ + OidcConfiguration: &application.UpdateOIDCApplicationConfigurationRequest{ + RedirectUris: wr.OIDCApp.Spec.RedirectUris, + ResponseTypes: responseTypes, + GrantTypes: grantTypes, + AuthMethodType: ptr.To(application.OIDCAuthMethodType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AuthMethodType])), + PostLogoutRedirectUris: wr.OIDCApp.Spec.PostLogoutRedirectUris, + Version: ptr.To(application.OIDCVersion_OIDC_VERSION_1_0), + DevelopmentMode: &wr.OIDCApp.Spec.DevMode, + AccessTokenType: ptr.To(application.OIDCTokenType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AccessTokenType])), + AccessTokenRoleAssertion: &wr.OIDCApp.Spec.AccessTokenRoleAssertion, + IdTokenRoleAssertion: &wr.OIDCApp.Spec.IdTokenRoleAssertion, + IdTokenUserinfoAssertion: &wr.OIDCApp.Spec.IdTokenUserinfoAssertion, + ClockSkew: durationpb.New(wr.OIDCApp.Spec.ClockSkew.Duration), + AdditionalOrigins: wr.OIDCApp.Spec.AdditionalOrigins, + SkipNativeAppSuccessPage: &wr.OIDCApp.Spec.SkipNativeAppSuccessPage, + LoginVersion: &application.LoginVersion{ + Version: &application.LoginVersion_LoginV2{ + LoginV2: &application.LoginV2{ + BaseUri: nil, + }, + }, + }, + BackChannelLogoutUri: &wr.OIDCApp.Spec.BackChannelLogoutUri, + }, + }, + }, + ) + if err != nil { + return fmt.Errorf("error updating OIDCApp in Zitadel: %v", err) + } + } + patch := ctrlClient.MergeFrom(wr.OIDCApp.DeepCopy()) + wr.OIDCApp.Status.AppId = appid + wr.OIDCApp.Status.ClientId = clientid + return wr.Client.Status().Patch(ctx, wr.OIDCApp, patch) +} + +func (wr *wrappedOIDCAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.OIDCApp.DeepCopy()) + patcher(&wr.OIDCApp.Status) + + if err := wr.Client.Status().Patch(ctx, wr.OIDCApp, patch); err != nil { + return fmt.Errorf("error patching OIDCApp status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OIDCAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.OIDCApp{}). + Owns(&corev1.Secret{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/oidcapp_controller_finalizer.gold b/src/internal/controller/oidcapp_controller_finalizer.gold new file mode 100644 index 0000000..869405e --- /dev/null +++ b/src/internal/controller/oidcapp_controller_finalizer.gold @@ -0,0 +1,88 @@ +package controller + +import ( + "strings" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + + "context" + "fmt" + + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/application/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + OIDCAppFinalizerName = "oidcapp.zitadel.topmanage.com/oidcapp" +) + +type wrappedOIDCAppFinalizer struct { + client.Client + OIDCApp *zitadelv1alpha1.OIDCApp + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedOIDCAppFinalizer(client client.Client, OIDCApp *zitadelv1alpha1.OIDCApp, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer { + return &wrappedOIDCAppFinalizer{ + Client: client, + OIDCApp: OIDCApp, + refresolver: refresolver, + } +} + +func (wf *wrappedOIDCAppFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.OIDCApp, func(OIDCApp *zitadelv1alpha1.OIDCApp) { + controllerutil.AddFinalizer(OIDCApp, OIDCAppFinalizerName) + }) +} + +func (wf *wrappedOIDCAppFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.OIDCApp, func(OIDCApp *zitadelv1alpha1.OIDCApp) { + controllerutil.RemoveFinalizer(wf.OIDCApp, OIDCAppFinalizerName) + }) +} + +func (wr *wrappedOIDCAppFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.OIDCApp, OIDCAppFinalizerName) +} + +func (wf *wrappedOIDCAppFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + if wf.OIDCApp.Status.AppId != nil { + project, err := wf.OIDCApp.Project(ctx, wf.refresolver) + if err != nil { + return err + } + _, err = ztdClient.ApplicationServiceV2().DeleteApplication(ctx, &application.DeleteApplicationRequest{ + ApplicationId: *wf.OIDCApp.Status.AppId, + ProjectId: *project.Status.ProjectId, + }) + if err != nil { + if strings.Contains(err.Error(), "doesn't exist") { + return nil + } + return err + } + } + return nil +} + +func (wr *wrappedOIDCAppFinalizer) patch(ctx context.Context, OIDCApp *zitadelv1alpha1.OIDCApp, + patchFn func(*zitadelv1alpha1.OIDCApp)) error { + patch := ctrlClient.MergeFrom(OIDCApp.DeepCopy()) + patchFn(OIDCApp) + + if err := wr.Client.Patch(ctx, OIDCApp, patch); err != nil { + return fmt.Errorf("error patching OIDCApp finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/organization_controller.gold b/src/internal/controller/organization_controller.gold new file mode 100644 index 0000000..376566a --- /dev/null +++ b/src/internal/controller/organization_controller.gold @@ -0,0 +1,167 @@ +/* +Copyright 2024. + +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" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/object/v2" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/org/v2" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// OrganizationReconciler reconciles a Organization object +type OrganizationReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration +} + +func NewOrganizationReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, conditionReady *condition.Ready, + requeueInterval time.Duration) *OrganizationReconciler { + return &OrganizationReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=organizations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=organizations/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=organizations/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. +func (r *OrganizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var organization zitadelv1alpha1.Organization + if err := r.Get(ctx, req.NamespacedName, &organization); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedOrganizationReconciler(r.Client, r.RefResolver, &organization) + wf := newWrappedOrganizationFinalizer(r.Client, &organization) + tf := core.NewCoreFinalizer(r.Client, wf) + tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &organization) + if err != nil { + return result, fmt.Errorf("error reconciling in OrganizationReconciler: %v", err) + } + return result, nil +} + +type wrappedOrganizationReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + organization *zitadelv1alpha1.Organization +} + +func newWrappedOrganizationReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, + organization *zitadelv1alpha1.Organization) core.WrappedCoreReconciler { + return &wrappedOrganizationReconciler{ + Client: client, + refResolver: refResolver, + organization: organization, + } +} + +type orgReconcilePhase struct { + Name string + Reconcile func(context.Context, *clientv2.Client) error +} + +func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + phases := []orgReconcilePhase{ + { + Name: "organization", + Reconcile: wr.reconcileOrg, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedOrganizationReconciler) reconcileOrg(ctx context.Context, ztdClient *clientv2.Client) error { + var organizationId *string + orgList, err := ztdClient.OrganizationServiceV2().ListOrganizations(ctx, &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + { + Query: &org.SearchQuery_NameQuery{ + NameQuery: &org.OrganizationNameQuery{ + Name: wr.organization.Spec.OrganzationName, + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("error listing organization: %v", err) + } + if len(orgList.Result) > 0 { + organizationId = &orgList.Result[0].Id + } + + if organizationId == nil { + resp, err := + ztdClient.OrganizationServiceV2().AddOrganization(ctx, &org.AddOrganizationRequest{ + Name: wr.organization.Spec.OrganzationName, + }) + if err != nil { + return fmt.Errorf("error creating organization: %v", err) + } + organizationId = &resp.OrganizationId + } + patch := ctrlClient.MergeFrom(wr.organization.DeepCopy()) + wr.organization.Status.OrganizationId = organizationId + return wr.Client.Status().Patch(ctx, wr.organization, patch) +} + +func (wr *wrappedOrganizationReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.organization.DeepCopy()) + patcher(&wr.organization.Status) + + if err := wr.Client.Status().Patch(ctx, wr.organization, patch); err != nil { + return fmt.Errorf("error patching Organization status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Organization{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/organization_controller_finalizer.gold b/src/internal/controller/organization_controller_finalizer.gold new file mode 100644 index 0000000..e3af8a9 --- /dev/null +++ b/src/internal/controller/organization_controller_finalizer.gold @@ -0,0 +1,77 @@ +package controller + +import ( + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + + "context" + "fmt" + + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/org/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + organizationFinalizerName = "organization.zitadel.github.com/organization" +) + +type wrappedOrganizationFinalizer struct { + client.Client + organization *zitadelv1alpha1.Organization +} + +func newWrappedOrganizationFinalizer(client client.Client, organization *zitadelv1alpha1.Organization) core.WrappedCoreFinalizer { + return &wrappedOrganizationFinalizer{ + Client: client, + organization: organization, + } +} + +func (wf *wrappedOrganizationFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.organization, func(organization *zitadelv1alpha1.Organization) { + controllerutil.AddFinalizer(organization, organizationFinalizerName) + }) +} + +func (wf *wrappedOrganizationFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.organization, func(organization *zitadelv1alpha1.Organization) { + controllerutil.RemoveFinalizer(wf.organization, organizationFinalizerName) + }) +} + +func (wr *wrappedOrganizationFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.organization, organizationFinalizerName) +} + +func (wf *wrappedOrganizationFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + if wf.organization.Status.OrganizationId != nil { + if _, err := ztdClient.OrganizationServiceV2().DeleteOrganization(ctx, + &org.DeleteOrganizationRequest{ + OrganizationId: *wf.organization.Status.OrganizationId, + }, + ); err != nil { + return fmt.Errorf("Error deleting organization: %v", err) + } + } + return nil +} + +func (wr *wrappedOrganizationFinalizer) patch(ctx context.Context, organization *zitadelv1alpha1.Organization, + patchFn func(*zitadelv1alpha1.Organization)) error { + patch := ctrlClient.MergeFrom(organization.DeepCopy()) + patchFn(organization) + + if err := wr.Client.Patch(ctx, organization, patch); err != nil { + return fmt.Errorf("error patching Organization finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/project_controller.gold b/src/internal/controller/project_controller.gold new file mode 100644 index 0000000..be7ee70 --- /dev/null +++ b/src/internal/controller/project_controller.gold @@ -0,0 +1,314 @@ +/* +Copyright 2024. + +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" + "reflect" + "sort" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/project/v2" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ProjectReconciler reconciles a Project object +type ProjectReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration +} + +func NewProjectReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, conditionReady *condition.Ready, + requeueInterval time.Duration) *ProjectReconciler { + return &ProjectReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=projects,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=projects/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=projects/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. +func (r *ProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var project zitadelv1alpha1.Project + if err := r.Get(ctx, req.NamespacedName, &project); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedProjectReconciler(r.Client, r.RefResolver, &project) + wf := newWrappedProjectFinalizer(r.Client, &project, r.RefResolver) + tf := core.NewCoreFinalizer(r.Client, wf) + tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &project) + if err != nil { + return result, fmt.Errorf("error reconciling in ProjectReconciler: %v", err) + } + return result, nil +} + +type wrappedProjectReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + project *zitadelv1alpha1.Project +} + +func newWrappedProjectReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, + project *zitadelv1alpha1.Project) core.WrappedCoreReconciler { + return &wrappedProjectReconciler{ + Client: client, + refResolver: refResolver, + project: project, + } +} + +type projectReconcilePhase struct { + Name string + Reconcile func(context.Context, *clientv2.Client) error +} + +func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + phases := []projectReconcilePhase{ + { + Name: "project", + Reconcile: wr.reconcileProject, + }, + { + Name: "roles", + Reconcile: wr.reconcileRoles, + }, + { + Name: "grants", + Reconcile: wr.reconcileGrants, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedProjectReconciler) reconcileProject(ctx context.Context, ztdClient *clientv2.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace) + if err != nil { + return err + } + if org.Status.OrganizationId == nil { + return fmt.Errorf("Organization not created yet") + } + var projectId *string + projectList, err := ztdClient.ProjectServiceV2().ListProjects(ctx, &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + &project.ProjectSearchFilter{ + Filter: &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: wr.project.Spec.ProjectName, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + &project.ProjectSearchFilter{ + Filter: &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ + OrganizationId: *org.Status.OrganizationId, + Type: project.ProjectOrganizationIDFilter_OWNED, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("error listing project: %v", err) + } + if len(projectList.Projects) > 0 { + projectId = &projectList.Projects[0].ProjectId + } + + if projectId == nil { + resp, err := + ztdClient.ProjectServiceV2().CreateProject(ctx, + &project.CreateProjectRequest{ + OrganizationId: *org.Status.OrganizationId, + Name: wr.project.Spec.ProjectName, + ProjectRoleAssertion: wr.project.Spec.ProjectRoleAssertion, + AuthorizationRequired: wr.project.Spec.ProjectRoleCheck, + ProjectAccessRequired: wr.project.Spec.HasProjectCheck, + }, + ) + if err != nil { + return fmt.Errorf("error creating project: %v", err) + } + projectId = &resp.ProjectId + } + patch := ctrlClient.MergeFrom(wr.project.DeepCopy()) + wr.project.Status.ProjectId = projectId + return wr.Client.Status().Patch(ctx, wr.project, patch) +} + +func (wr *wrappedProjectReconciler) reconcileRoles(ctx context.Context, ztdClient *clientv2.Client) error { + resp, err := ztdClient.ProjectServiceV2().ListProjectRoles(ctx, &project.ListProjectRolesRequest{ + ProjectId: *wr.project.Status.ProjectId, + }) + if err != nil { + return fmt.Errorf("Could not list project roles: %v", err) + } + roles := map[string]*project.ProjectRole{} + deleteRolesKeys := []string{} + for _, role := range wr.project.Spec.Roles { + roles[role.Key] = &project.ProjectRole{ + Key: role.Key, + DisplayName: role.DisplayName, + Group: role.Group, + ProjectId: *wr.project.Status.ProjectId, + } + } + + for _, role := range resp.ProjectRoles { + if r, ok := roles[role.Key]; ok { + if r.DisplayName != role.DisplayName || r.Group != role.Group { + deleteRolesKeys = append(deleteRolesKeys, role.Key) + } else { + delete(roles, role.Key) + } + } else { + deleteRolesKeys = append(deleteRolesKeys, role.Key) + } + } + + if len(deleteRolesKeys) > 0 { + for _, key := range deleteRolesKeys { + if _, err = ztdClient.ProjectServiceV2().RemoveProjectRole(ctx, &project.RemoveProjectRoleRequest{ + ProjectId: *wr.project.Status.ProjectId, + RoleKey: key, + }); err != nil { + return fmt.Errorf("Error removing project role: %v", err) + } + } + } + + if len(roles) > 0 { + for _, value := range roles { + if _, err = ztdClient.ProjectServiceV2().AddProjectRole(ctx, &project.AddProjectRoleRequest{ + ProjectId: *wr.project.Status.ProjectId, + RoleKey: value.Key, + DisplayName: value.DisplayName, + Group: &value.Group, + }); err != nil { + return fmt.Errorf("Error adding project role: %v", err) + } + } + } + return nil +} + +func (wr *wrappedProjectReconciler) reconcileGrants(ctx context.Context, ztdClient *clientv2.Client) error { + existingGrants, err := ztdClient.ProjectServiceV2().ListProjectGrants(ctx, &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{ + { + Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{ + *wr.project.Status.ProjectId, + }, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("Error listing project grants: %v", err) + } + for _, grant := range wr.project.DeepCopy().Spec.Grants { + grantedOrg, err := wr.refResolver.OrganizationRef(ctx, &grant.OrganizationRef, wr.project.Namespace) + if err != nil { + return err + } + if grantedOrg.Status.OrganizationId == nil { + continue + } + var existingGrant *project.ProjectGrant + for _, eGrant := range existingGrants.ProjectGrants { + if eGrant.GrantedOrganizationId == *grantedOrg.Status.OrganizationId { + existingGrant = eGrant + break + } + } + if existingGrant == nil { + _, err := ztdClient.ProjectServiceV2().CreateProjectGrant(ctx, &project.CreateProjectGrantRequest{ + ProjectId: *wr.project.Status.ProjectId, + GrantedOrganizationId: *grantedOrg.Status.OrganizationId, + RoleKeys: grant.RoleKeys, + }) + if err != nil { + return fmt.Errorf("Error Adding project grant: %v", err) + } + } else { + sort.Strings(existingGrant.GrantedRoleKeys) + sort.Strings(grant.RoleKeys) + if !reflect.DeepEqual(existingGrant.GrantedRoleKeys, grant.RoleKeys) { + _, err := ztdClient.ProjectServiceV2().UpdateProjectGrant(ctx, &project.UpdateProjectGrantRequest{ + ProjectId: *wr.project.Status.ProjectId, + GrantedOrganizationId: existingGrant.GrantedOrganizationId, + RoleKeys: grant.RoleKeys, + }) + if err != nil { + return fmt.Errorf("Error Updating project grant: %v", err) + } + } + } + } + return nil +} + +func (wr *wrappedProjectReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.project.DeepCopy()) + patcher(&wr.project.Status) + + if err := wr.Client.Status().Patch(ctx, wr.project, patch); err != nil { + return fmt.Errorf("error patching Project status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ProjectReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Project{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/project_controller_finalizer.gold b/src/internal/controller/project_controller_finalizer.gold new file mode 100644 index 0000000..06cc01c --- /dev/null +++ b/src/internal/controller/project_controller_finalizer.gold @@ -0,0 +1,76 @@ +package controller + +import ( + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" + "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/core" + + "context" + "fmt" + + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/project/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + projectFinalizerName = "project.zitadel.github.com/project" +) + +type wrappedProjectFinalizer struct { + client.Client + project *zitadelv1alpha1.Project + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedProjectFinalizer(client client.Client, project *zitadelv1alpha1.Project, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer { + return &wrappedProjectFinalizer{ + Client: client, + project: project, + refresolver: refresolver, + } +} + +func (wf *wrappedProjectFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.project, func(project *zitadelv1alpha1.Project) { + controllerutil.AddFinalizer(project, projectFinalizerName) + }) +} + +func (wf *wrappedProjectFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.project, func(project *zitadelv1alpha1.Project) { + controllerutil.RemoveFinalizer(wf.project, projectFinalizerName) + }) +} + +func (wr *wrappedProjectFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.project, projectFinalizerName) +} + +func (wf *wrappedProjectFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + if wf.project.Status.ProjectId != nil { + _, err := ztdClient.ProjectServiceV2().DeleteProject(ctx, &project.DeleteProjectRequest{ProjectId: *wf.project.Status.ProjectId}) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedProjectFinalizer) patch(ctx context.Context, project *zitadelv1alpha1.Project, + patchFn func(*zitadelv1alpha1.Project)) error { + patch := ctrlClient.MergeFrom(project.DeepCopy()) + patchFn(project) + + if err := wr.Client.Patch(ctx, project, patch); err != nil { + return fmt.Errorf("error patching Project finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/suite_test.go b/src/internal/controller/suite_test.go new file mode 100644 index 0000000..cf2f829 --- /dev/null +++ b/src/internal/controller/suite_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2024. + +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 ( + "path/filepath" + "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" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/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 + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = zitadelv1alpha1.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") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/src/pkg/builder/builder.go b/src/pkg/builder/builder.go new file mode 100644 index 0000000..4a3d090 --- /dev/null +++ b/src/pkg/builder/builder.go @@ -0,0 +1,15 @@ +package builder + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +type Builder struct { + scheme *runtime.Scheme +} + +func NewBuilder(scheme *runtime.Scheme) *Builder { + return &Builder{ + scheme: scheme, + } +} diff --git a/src/pkg/builder/labels/labels.go b/src/pkg/builder/labels/labels.go new file mode 100644 index 0000000..fef3abf --- /dev/null +++ b/src/pkg/builder/labels/labels.go @@ -0,0 +1,21 @@ +package builder + +type LabelsBuilder struct { + labels map[string]string +} + +func NewLabelsBuilder() *LabelsBuilder { + return &LabelsBuilder{ + labels: map[string]string{}, + } +} +func (b *LabelsBuilder) WithLabels(labels map[string]string) *LabelsBuilder { + for k, v := range labels { + b.labels[k] = v + } + return b +} + +func (b *LabelsBuilder) Build() map[string]string { + return b.labels +} diff --git a/src/pkg/builder/metadata/metadata.go b/src/pkg/builder/metadata/metadata.go new file mode 100644 index 0000000..75af2cb --- /dev/null +++ b/src/pkg/builder/metadata/metadata.go @@ -0,0 +1,39 @@ +package metadata + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +type MetadataBuilder struct { + objMeta metav1.ObjectMeta +} + +func NewMetadataBuilder(key types.NamespacedName) *MetadataBuilder { + return &MetadataBuilder{ + objMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + } +} + +func (b *MetadataBuilder) WithLabels(labels map[string]string) *MetadataBuilder { + for k, v := range labels { + b.objMeta.Labels[k] = v + } + return b +} + +func (b *MetadataBuilder) WithAnnotations(annotations map[string]string) *MetadataBuilder { + for k, v := range annotations { + b.objMeta.Annotations[k] = v + } + return b +} + +func (b *MetadataBuilder) Build() metav1.ObjectMeta { + return b.objMeta +} diff --git a/src/pkg/builder/secret_builder.go b/src/pkg/builder/secret_builder.go new file mode 100644 index 0000000..77832b1 --- /dev/null +++ b/src/pkg/builder/secret_builder.go @@ -0,0 +1,35 @@ +package builder + +import ( + "fmt" + metadata "github.com/HaimKortovich/zitadel-resources-operator/pkg/builder/metadata" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type SecretOpts struct { + Key types.NamespacedName + Data map[string][]byte + Labels map[string]string + Annotations map[string]string + Immutable bool +} + +func (b *Builder) BuildSecret(opts SecretOpts, owner metav1.Object) (*corev1.Secret, error) { + objMeta := + metadata.NewMetadataBuilder(opts.Key). + WithLabels(opts.Labels). + WithAnnotations(opts.Annotations). + Build() + secret := &corev1.Secret{ + ObjectMeta: objMeta, + Data: opts.Data, + Immutable: &opts.Immutable, + } + if err := controllerutil.SetControllerReference(owner, secret, b.scheme); err != nil { + return nil, fmt.Errorf("error setting controller reference in Secret manifest: %v", err) + } + return secret, nil +} diff --git a/src/pkg/condition/condition.go b/src/pkg/condition/condition.go new file mode 100644 index 0000000..bacf590 --- /dev/null +++ b/src/pkg/condition/condition.go @@ -0,0 +1,68 @@ +package conditions + +import ( + "fmt" + "reflect" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Conditioner interface { + SetCondition(condition metav1.Condition) +} + +type Patcher func(Conditioner) + +type Ready struct{} + +func NewReady() *Ready { + return &Ready{} +} + +func (p *Ready) PatcherFailed(msg string) Patcher { + return func(c Conditioner) { + SetReadyFailedWithMessage(c, msg) + } +} + +func (p *Ready) PatcherWithError(err error) Patcher { + return func(c Conditioner) { + if err == nil { + SetReadyCreated(c) + } else { + SetReadyFailed(c) + } + } +} + +func (p *Ready) PatcherRefResolver(err error, obj interface{}) Patcher { + return func(c Conditioner) { + if err == nil { + return + } + if apierrors.IsNotFound(err) { + SetReadyFailedWithMessage(c, fmt.Sprintf("%s not found", getType(obj))) + return + } + SetReadyFailedWithMessage(c, fmt.Sprintf("Error getting %s", getType(obj))) + } +} + +func (p *Ready) PatcherHealthy(err error) Patcher { + return func(c Conditioner) { + if err == nil { + SetReadyHealthty(c) + } else { + SetReadyUnhealthtyWithError(c, err) + } + } +} + +func getType(obj interface{}) string { + if t := reflect.TypeOf(obj); t.Kind() == reflect.Ptr { + return t.Elem().Name() + } else { + return t.Name() + } +} diff --git a/src/pkg/condition/pat.go b/src/pkg/condition/pat.go new file mode 100644 index 0000000..971d1e7 --- /dev/null +++ b/src/pkg/condition/pat.go @@ -0,0 +1,25 @@ +package conditions + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" +) + +func SetPatOutOfDate(c Conditioner) { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypePATUpToDate, + Status: metav1.ConditionFalse, + Reason: zitadelv1alpha1.ConditionReasonRolesChanged, + Message: "PAT out of date", + }) +} + +func SetPatUpToDate(c Conditioner) { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypePATUpToDate, + Status: metav1.ConditionTrue, + Reason: zitadelv1alpha1.ConditionReasonPATUpToDate, + Message: "PAT up to date", + }) +} diff --git a/src/pkg/condition/ready.go b/src/pkg/condition/ready.go new file mode 100644 index 0000000..14ee71e --- /dev/null +++ b/src/pkg/condition/ready.go @@ -0,0 +1,70 @@ +package conditions + +import ( + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" +) + +func SetReadyHealthty(c Conditioner) { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: zitadelv1alpha1.ConditionReasonHealthy, + Message: "Healthy", + }) +} + +func SetReadyUnhealthtyWithError(c Conditioner, err error) { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: zitadelv1alpha1.ConditionReasonHealthy, + Message: err.Error(), + }) +} + +func SetReadyCreatedWithMessage(c Conditioner, message string) { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: zitadelv1alpha1.ConditionReasonCreated, + Message: message, + }) +} + +func SetReadyCreated(c Conditioner) { + SetReadyCreatedWithMessage(c, "Created") +} + +func SetReadyFailedWithMessage(c Conditioner, message string) { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: zitadelv1alpha1.ConditionReasonFailed, + Message: message, + }) +} + +func SetReadyFailed(c Conditioner) { + SetReadyFailedWithMessage(c, "Failed") +} + +func SetReadyWithDeployment(c Conditioner, sts *appsv1.Deployment) { + if sts.Status.Replicas == 0 || sts.Status.ReadyReplicas != sts.Status.Replicas { + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: zitadelv1alpha1.ConditionReasonDeploymentNotReady, + Message: "Not ready", + }) + return + } + c.SetCondition(metav1.Condition{ + Type: zitadelv1alpha1.ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: zitadelv1alpha1.ConditionReasonDeploymentReady, + Message: "Running", + }) +} diff --git a/src/pkg/controller/core/controller.go b/src/pkg/controller/core/controller.go new file mode 100644 index 0000000..df65362 --- /dev/null +++ b/src/pkg/controller/core/controller.go @@ -0,0 +1,111 @@ +package core + +import ( + "context" + "fmt" + "time" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + condition "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition" + zitadelClient "github.com/HaimKortovich/zitadel-resources-operator/pkg/zitadel" + "github.com/hashicorp/go-multierror" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type CoreReconciler struct { + Client client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + + WrappedReconciler WrappedCoreReconciler + Finalizer Finalizer + RequeueInterval time.Duration +} + +func NewCoreReconciler(client client.Client, cr *condition.Ready, wr WrappedCoreReconciler, f Finalizer, + requeueInterval time.Duration) Reconciler { + return &CoreReconciler{ + Client: client, + RefResolver: zitadelv1alpha1.NewRefResolver(client), + ConditionReady: cr, + WrappedReconciler: wr, + Finalizer: f, + RequeueInterval: requeueInterval, + } +} + +func (r *CoreReconciler) Reconcile(ctx context.Context, resource Resource) (ctrl.Result, error) { + if resource.IsBeingDeleted() { + if err := r.Finalizer.Finalize(ctx, resource); err != nil { + return ctrl.Result{}, fmt.Errorf("error finalizing %s: %v", resource.GetName(), err) + } + return ctrl.Result{}, nil + } + connectionRef, err := resource.ConnectionRef(ctx, r.RefResolver) + if err != nil { + return ctrl.Result{}, err + } + connection, err := r.RefResolver.ConnectionRef(ctx, connectionRef, resource.GetNamespace()) + + if err != nil { + var errBundle *multierror.Error + errBundle = multierror.Append(errBundle, err) + + err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherRefResolver(err, connection)) + errBundle = multierror.Append(errBundle, err) + + return ctrl.Result{}, fmt.Errorf("error getting Connection: %v", errBundle) + } + + ztdClient, err := zitadelClient.NewV2Client(ctx, connection, *r.RefResolver) + if err != nil { + var errBundle *multierror.Error + errBundle = multierror.Append(errBundle, err) + + msg := fmt.Sprintf("Error connecting to Zitadel: %v", err) + err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherFailed(msg)) + errBundle = multierror.Append(errBundle, err) + + return r.retryResult(ctx, resource, errBundle) + } + + defer ztdClient.Close() + + err = r.WrappedReconciler.Reconcile(ctx, ztdClient) + var errBundle *multierror.Error + errBundle = multierror.Append(errBundle, err) + + if err := errBundle.ErrorOrNil(); err != nil { + msg := fmt.Sprintf("Error creating %s: %v", resource.GetName(), err) + err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherFailed(msg)) + errBundle = multierror.Append(errBundle, err) + + return r.retryResult(ctx, resource, errBundle) + } + + if err = r.Finalizer.AddFinalizer(ctx); err != nil { + errBundle = multierror.Append(errBundle, fmt.Errorf("error adding finalizer to %s: %v", resource.GetName(), err)) + } + + err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherWithError(errBundle.ErrorOrNil())) + errBundle = multierror.Append(errBundle, err) + + if err := errBundle.ErrorOrNil(); err != nil { + return ctrl.Result{}, err + } + return r.requeueResult(ctx, resource) +} + +func (r *CoreReconciler) retryResult(ctx context.Context, resource Resource, err error) (ctrl.Result, error) { + return ctrl.Result{}, err +} + +func (r *CoreReconciler) requeueResult(ctx context.Context, resource Resource) (ctrl.Result, error) { + if r.RequeueInterval > 0 { + log.FromContext(ctx).V(1).Info("Requeuing CORE resource") + return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil + } + return ctrl.Result{}, nil +} diff --git a/src/pkg/controller/core/finalizer.go b/src/pkg/controller/core/finalizer.go new file mode 100644 index 0000000..0e1e8d1 --- /dev/null +++ b/src/pkg/controller/core/finalizer.go @@ -0,0 +1,73 @@ +package core + +import ( + "context" + "fmt" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + zitadelClient "github.com/HaimKortovich/zitadel-resources-operator/pkg/zitadel" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type CoreFinalizer struct { + Client client.Client + RefResolver *zitadelv1alpha1.RefResolver + + WrappedFinalizer WrappedCoreFinalizer +} + +func NewCoreFinalizer(client client.Client, wf WrappedCoreFinalizer) Finalizer { + return &CoreFinalizer{ + Client: client, + RefResolver: zitadelv1alpha1.NewRefResolver(client), + WrappedFinalizer: wf, + } +} + +func (tf *CoreFinalizer) AddFinalizer(ctx context.Context) error { + if tf.WrappedFinalizer.ContainsFinalizer() { + return nil + } + if err := tf.WrappedFinalizer.AddFinalizer(ctx); err != nil { + return fmt.Errorf("error adding finalizer in TemplateFinalizer: %v", err) + } + return nil +} + +func (tf *CoreFinalizer) Finalize(ctx context.Context, resource Resource) error { + if !tf.WrappedFinalizer.ContainsFinalizer() { + return nil + } + + connectionRef, err := resource.ConnectionRef(ctx, tf.RefResolver) + if err != nil { + return err + } + + connection, err := tf.RefResolver.ConnectionRef(ctx, connectionRef, resource.GetNamespace()) + if err != nil { + if apierrors.IsNotFound(err) { + if err := tf.WrappedFinalizer.RemoveFinalizer(ctx); err != nil { + return fmt.Errorf("error removing %s finalizer: %v", resource.GetName(), err) + } + return nil + } + return fmt.Errorf("error getting Cluster: %v", err) + } + + ztdClient, err := zitadelClient.NewV2Client(ctx, connection, *tf.RefResolver) + if err != nil { + return fmt.Errorf("error connecting to Zitadel: %v", err) + } + defer ztdClient.Close() + + if err := tf.WrappedFinalizer.Reconcile(ctx, ztdClient); err != nil { + return fmt.Errorf("error reconciling in TemplateFinalizer: %v", err) + } + + if err := tf.WrappedFinalizer.RemoveFinalizer(ctx); err != nil { + return fmt.Errorf("error removing finalizer in TemplateFinalizer: %v", err) + } + return nil +} diff --git a/src/pkg/controller/core/types.go b/src/pkg/controller/core/types.go new file mode 100644 index 0000000..15a9d1c --- /dev/null +++ b/src/pkg/controller/core/types.go @@ -0,0 +1,39 @@ +package core + +import ( + "context" + + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + "github.com/zitadel/zitadel-go/v3/pkg/client" + + condition "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +type Resource interface { + v1.Object + ConnectionRef(context.Context, *zitadelv1alpha1.RefResolver) (*zitadelv1alpha1.ConnectionRef, error) + IsBeingDeleted() bool +} + +type Reconciler interface { + Reconcile(ctx context.Context, resource Resource) (ctrl.Result, error) +} + +type WrappedCoreReconciler interface { + Reconcile(context.Context, *client.Client) error + PatchStatus(context.Context, condition.Patcher) error +} + +type Finalizer interface { + AddFinalizer(context.Context) error + Finalize(context.Context, Resource) error +} + +type WrappedCoreFinalizer interface { + AddFinalizer(context.Context) error + RemoveFinalizer(context.Context) error + ContainsFinalizer() bool + Reconcile(context.Context, *client.Client) error +} diff --git a/src/pkg/zitadel/zitadel.go b/src/pkg/zitadel/zitadel.go new file mode 100644 index 0000000..b580842 --- /dev/null +++ b/src/pkg/zitadel/zitadel.go @@ -0,0 +1,63 @@ +package zitadel + +import ( + "context" + "fmt" + zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + z "github.com/zitadel/zitadel-go/v3/pkg/zitadel" +) + +func NewV2Client(ctx context.Context, connection *zitadelv1alpha1.Connection, refresolver zitadelv1alpha1.RefResolver) (*clientv2.Client, error) { + zOpts := []z.Option{} + if connection.Spec.Port != nil { + zOpts = append(zOpts, z.WithPort(*connection.Spec.Port)) + } + + if connection.Spec.InsecureSkipVerifyTLS { + zOpts = append(zOpts, z.WithInsecureSkipVerifyTLS()) + } + + if !connection.Spec.Secure { + port := uint16(443) + if connection.Spec.Port != nil { + port = *connection.Spec.Port + } + zOpts = append(zOpts, z.WithInsecure(fmt.Sprintf("%d", port))) + } + + var auth clientv2.TokenSourceInitializer + if connection.Spec.Authentication.PAT != nil { + pat, err := refresolver.SecretKeyRef(ctx, connection.Spec.Authentication.PAT.TokenSecretKey, connection.Namespace) + if err != nil { + return nil, err + } + auth = clientv2.PAT(pat) + } + + if connection.Spec.Authentication.JWT != nil { + jwt, err := refresolver.SecretKeyRef(ctx, connection.Spec.Authentication.JWT.JWTSecretKey, connection.Namespace) + if err != nil { + return nil, err + } + keyfile, err := clientv2.ConfigFromKeyFileData([]byte(jwt)) + if err != nil { + return nil, err + } + auth = clientv2.AuthenticationJWTProfile(keyfile, connection.Spec.Authentication.JWT.Scopes...) + } + + if connection.Spec.Authentication.Password != nil { + password, err := refresolver.SecretKeyRef(ctx, connection.Spec.Authentication.Password.PasswordSecretKey, connection.Namespace) + if err != nil { + return nil, err + } + auth = clientv2.PasswordAuthentication(connection.Spec.Authentication.Password.Username, password, connection.Spec.Authentication.Password.Scopes...) + } + + client, err := clientv2.New(ctx, z.New(connection.Spec.Host, zOpts...), clientv2.WithAuth(auth)) + if err != nil { + return nil, fmt.Errorf("Error creating V2Client: %v", err) + } + return client, nil +}